This commit is contained in:
asp_ly 2024-12-23 21:41:14 +08:00
parent 278ada41eb
commit 90e9ab004c
6 changed files with 532 additions and 359 deletions

View File

@ -0,0 +1,43 @@
.environment-card {
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.ant-card-actions {
background: #fafafa;
border-top: 1px solid #f0f0f0;
li {
margin: 12px 0;
&:not(:last-child) {
border-right: 1px solid #f0f0f0;
}
}
}
.ant-card-body {
padding: 0;
}
.ant-tag {
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
opacity: 0.8;
}
}
.ant-typography {
margin-bottom: 0;
}
.ant-btn {
&:hover {
transform: scale(1.05);
transition: all 0.3s ease;
}
}
}

View File

@ -1,7 +1,7 @@
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, Popconfirm, Space, Tag, Tooltip } from 'antd'; import { Card, Row, Col, Typography, Button, message, Empty, Tooltip, Tag } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, RocketOutlined } 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';
@ -14,16 +14,19 @@ const buildTypeMap = {
[BuildTypeEnum.JENKINS]: { [BuildTypeEnum.JENKINS]: {
label: 'Jenkins构建', label: 'Jenkins构建',
color: '#1890ff', color: '#1890ff',
icon: <RocketOutlined />,
description: '使用Jenkins进行自动化构建和部署' description: '使用Jenkins进行自动化构建和部署'
}, },
[BuildTypeEnum.GITLAB_RUNNER]: { [BuildTypeEnum.GITLAB_RUNNER]: {
label: 'GitLab Runner构建', label: 'GitLab Runner构建',
color: '#722ed1', color: '#722ed1',
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 />,
description: '使用GitHub Action进行自动化工作流' description: '使用GitHub Action进行自动化工作流'
}, },
}; };
@ -32,16 +35,19 @@ const deployTypeMap = {
[DeployTypeEnum.K8S]: { [DeployTypeEnum.K8S]: {
label: 'Kubernetes集群部署', label: 'Kubernetes集群部署',
color: '#1890ff', color: '#1890ff',
icon: <CloudServerOutlined />,
description: '部署到Kubernetes容器编排平台' description: '部署到Kubernetes容器编排平台'
}, },
[DeployTypeEnum.DOCKER]: { [DeployTypeEnum.DOCKER]: {
label: 'Docker容器部署', label: 'Docker容器部署',
color: '#13c2c2', color: '#13c2c2',
icon: <RocketOutlined />,
description: '部署为Docker独立容器' description: '部署为Docker独立容器'
}, },
[DeployTypeEnum.VM]: { [DeployTypeEnum.VM]: {
label: '虚拟机部署', label: '虚拟机部署',
color: '#faad14', color: '#faad14',
icon: <SettingOutlined />,
description: '部署到传统虚拟机环境' description: '部署到传统虚拟机环境'
}, },
}; };
@ -97,65 +103,46 @@ const EnvironmentList: React.FC = () => {
hoverable hoverable
className="environment-card" className="environment-card"
style={{ style={{
height: '100%', borderRadius: '12px',
borderRadius: '16px',
overflow: 'hidden', 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)',
}}
bodyStyle={{
padding: '24px',
height: '100%', height: '100%',
display: 'flex', transition: 'all 0.3s ease',
flexDirection: 'column',
background: `linear-gradient(145deg, #ffffff 0%, ${buildType.color}08 100%)`,
}} }}
actions={[ actions={[
<Tooltip title="编辑环境配置"> <Tooltip title="编辑环境">
<Button <Button
type="text" type="text"
key="edit" icon={<EditOutlined />}
icon={<EditOutlined style={{ color: buildType.color }} />}
onClick={() => handleEdit(environment)} onClick={() => handleEdit(environment)}
> >
</Button> </Button>
</Tooltip>, </Tooltip>,
<Popconfirm <Tooltip title="删除环境">
key="delete"
title="确定要删除该环境吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(environment.id)}
>
<Button <Button
type="text" type="text"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => handleDelete(environment.id)}
> >
</Button> </Button>
</Popconfirm>, </Tooltip>
]} ]}
> >
<div style={{ flex: 1 }}> <div style={{ position: 'relative', padding: '16px' }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: '16px' marginBottom: '16px'
}}> }}>
<Title level={4} style={{ <Title level={4} style={{ margin: 0 }}>
margin: 0,
color: '#1f1f1f',
}}>
{environment.envName} {environment.envName}
</Title> </Title>
<RocketOutlined style={{ <Tag color={buildType.color} style={{ borderRadius: '12px', padding: '0 12px' }}>
fontSize: '24px', {buildType.icon} {buildType.label}
color: buildType.color, </Tag>
opacity: 0.8
}} />
</div> </div>
<Text <Text
@ -175,71 +162,45 @@ const EnvironmentList: React.FC = () => {
</Text> </Text>
<div style={{ <div style={{
display: 'flex', background: '#f5f5f5',
flexDirection: 'column', borderRadius: '8px',
gap: '16px', padding: '16px',
padding: '20px', marginBottom: '16px'
background: '#fff',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}> }}>
<div> <Text type="secondary"></Text>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{ <div style={{
color: '#262626',
fontFamily: 'monaco, monospace', fontFamily: 'monaco, monospace',
fontSize: '14px', fontSize: '14px',
padding: '8px 12px', color: '#262626',
background: '#f5f5f5', marginTop: '8px',
borderRadius: '6px', padding: '8px',
letterSpacing: '0.5px', background: '#fff',
borderRadius: '4px',
border: '1px solid #f0f0f0'
}}> }}>
{environment.envCode} {environment.envCode}
</div> </div>
</div> </div>
<Space direction="vertical" size="middle" style={{ width: '100%' }}> <div style={{
<div> display: 'flex',
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text> justifyContent: 'space-between',
<Tooltip title={buildType.description}> alignItems: 'center',
<Tag marginTop: '16px'
color={buildType.color} }}>
style={{
padding: '4px 12px',
borderRadius: '4px',
cursor: 'help'
}}
>
{buildType.label}
</Tag>
</Tooltip>
</div>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<Tooltip title={deployType.description}> <Tooltip title={deployType.description}>
<Tag <Tag
color={deployType.color} color={deployType.color}
style={{ style={{
padding: '4px 12px', padding: '4px 12px',
borderRadius: '4px', borderRadius: '12px',
cursor: 'help' cursor: 'help'
}} }}
> >
{deployType.label} {deployType.icon} {deployType.label}
</Tag> </Tag>
</Tooltip> </Tooltip>
</div> <Text type="secondary">: {environment.sort}</Text>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontSize: '14px',
padding: '4px 0',
}}>
{environment.sort}
</div>
</div>
</Space>
</div> </div>
</div> </div>
</Card> </Card>
@ -258,9 +219,10 @@ const EnvironmentList: React.FC = () => {
onClick={handleAdd} onClick={handleAdd}
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
padding: '0 24px', padding: '0 24px',
height: '40px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
transition: 'all 0.3s ease',
}} }}
> >
@ -270,47 +232,39 @@ const EnvironmentList: React.FC = () => {
> >
<div style={{ padding: '24px' }}> <div style={{ padding: '24px' }}>
<Row gutter={[24, 24]}> <Row gutter={[24, 24]}>
{environments.map(environment => ( {environments.length > 0 ? (
<Col xs={24} sm={12} md={8} lg={6} key={environment.id}> environments.map((environment) => (
<Col key={environment.id} xs={24} sm={12} md={8} lg={6}>
<EnvironmentCard environment={environment} /> <EnvironmentCard environment={environment} />
</Col> </Col>
))} ))
<Col xs={24} sm={12} md={8} lg={6}> ) : (
<Card <Col span={24}>
hoverable <Empty
onClick={handleAdd} image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无环境数据"
style={{ style={{
height: '100%', background: '#fff',
borderRadius: '16px', padding: '40px',
border: '1px dashed #d9d9d9', borderRadius: '8px',
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',
}} }}
> >
<div style={{ textAlign: 'center' }}> <Button type="primary" onClick={handleAdd}>
<PlusOutlined style={{ fontSize: '28px', color: '#8c8c8c' }} />
<div style={{ marginTop: '12px', color: '#8c8c8c', fontSize: '16px' }}></div> </Button>
</div> </Empty>
</Card>
</Col> </Col>
)}
</Row> </Row>
</div> </div>
<EnvironmentModal <EnvironmentModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={fetchEnvironments} onSuccess={() => {
setModalVisible(false);
fetchEnvironments();
}}
initialValues={currentEnvironment} initialValues={currentEnvironment}
/> />
</PageContainer> </PageContainer>

View File

@ -5,14 +5,14 @@ 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 ProjectModalProps { interface ProjectGroupModalProps {
visible: boolean; visible: boolean;
onCancel: () => void; onCancel: () => void;
onSuccess: () => void; onSuccess: () => void;
initialValues?: ProjectGroup; initialValues?: ProjectGroup;
} }
const ProjectModal: React.FC<ProjectModalProps> = ({ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
visible, visible,
onCancel, onCancel,
onSuccess, onSuccess,
@ -83,6 +83,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
destroyOnClose destroyOnClose
width={560}
> >
<Form <Form
form={form} form={form}
@ -155,4 +156,4 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
); );
}; };
export default ProjectModal; export default ProjectGroupModal;

View File

@ -0,0 +1,171 @@
.project-card {
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.ant-card-actions {
background: #fafafa;
border-top: 1px solid #f0f0f0;
li {
margin: 12px 0;
&:not(:last-child) {
border-right: 1px solid #f0f0f0;
}
}
}
.ant-card-body {
padding: 0;
}
.ant-tag {
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
opacity: 0.8;
}
}
.ant-typography {
margin-bottom: 0;
}
.ant-btn {
&:hover {
transform: scale(1.05);
transition: all 0.3s ease;
}
}
.ant-space {
.anticon {
font-size: 14px;
}
}
.environment-section {
.section-header {
margin-bottom: 16px;
}
}
.environment-list {
.environment-item {
background: #fff;
border-radius: 8px;
padding: 16px;
border: 1px solid #f0f0f0;
margin-bottom: 12px;
transition: all 0.3s ease;
&:last-child {
margin-bottom: 0;
}
&:hover {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-color: #d9d9d9;
}
.env-header {
.env-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.env-tags {
display: flex;
gap: 6px;
.env-tag {
margin: 0;
padding: 4px 8px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
.anticon {
font-size: 14px;
}
}
}
.env-meta {
display: flex;
align-items: center;
gap: 12px;
.env-code {
font-family: 'monaco, monospace';
color: #666;
background: #f5f5f5;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin: 0;
}
.env-desc {
font-size: 12px;
color: #8c8c8c;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: help;
}
}
}
.env-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #f0f0f0;
.anticon {
opacity: 0.6;
}
}
}
}
.info-section {
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
transition: all 0.3s ease;
&:hover {
background: #f0f0f0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.section-content {
background: #fff;
border-radius: 4px;
border: 1px solid #f0f0f0;
padding: 8px;
&.code {
font-family: 'monaco, monospace';
font-size: 14px;
color: #262626;
}
}
}
}

View File

@ -1,19 +1,11 @@
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, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider } from 'antd'; import { Card, Row, Col, Typography, Button, message, Input, Select, Empty, Tooltip, Tag, Space } from 'antd';
import { import { PlusOutlined, SearchOutlined, RocketOutlined, TeamOutlined, EnvironmentOutlined, EditOutlined, DeleteOutlined, ClockCircleOutlined, CloudServerOutlined, SettingOutlined } from '@ant-design/icons';
PlusOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
SearchOutlined,
EnvironmentOutlined,
ClockCircleOutlined,
RocketOutlined
} from '@ant-design/icons';
import { getProjectGroupList, deleteProjectGroup } from './service'; import { getProjectGroupList, deleteProjectGroup } from './service';
import type { ProjectGroup } from './types'; import type { ProjectGroup } from './types';
import ProjectModal from './components/ProjectModal'; 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;
@ -91,80 +83,118 @@ const ProjectList: React.FC = () => {
}; };
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 : const matchesType = projectType === 'ALL' ? true : getProjectTypeInfo(project.projectGroupCode).type === projectType;
getProjectTypeInfo(project.projectGroupCode).type === projectType;
return matchesSearch && matchesType; 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 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={{
height: '100%', borderRadius: '12px',
borderRadius: '16px',
overflow: 'hidden', 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%', height: '100%',
display: 'flex', transition: 'all 0.3s ease',
flexDirection: 'column',
background: `linear-gradient(145deg, #ffffff 0%, ${typeInfo.color}08 100%)`,
}} }}
actions={[
<Tooltip title="编辑项目组">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(project)}
> >
<div style={{ flex: 1 }}>
</Button>
</Tooltip>,
<Tooltip title="删除项目组">
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(project.id)}
>
</Button>
</Tooltip>,
<Tooltip title="管理成员">
<Button
type="text"
icon={<TeamOutlined />}
>
</Button>
</Tooltip>
]}
>
<div style={{ position: 'relative', padding: '16px' }}>
<div style={{ <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', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px' justifyContent: 'space-between',
marginBottom: '16px'
}}> }}>
<Title level={4} style={{ margin: 0 }}>
{project.projectGroupName} {project.projectGroupName}
<Tag
color={typeInfo.color}
style={{
margin: 0,
padding: '0 8px',
borderRadius: '4px',
fontSize: '12px'
}}
>
{typeInfo.label}
</Tag>
</Title> </Title>
<Tooltip title={typeInfo.description}>
<Tag color={typeInfo.color} style={{ borderRadius: '12px', padding: '0 12px' }}>
{typeInfo.icon} {typeInfo.label}
</Tag>
</Tooltip>
</div>
<Text <Text
type="secondary" type="secondary"
style={{ style={{
display: 'block', display: 'block',
marginTop: '8px', marginBottom: '24px',
height: '40px', height: '40px',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@ -175,122 +205,101 @@ const ProjectList: React.FC = () => {
> >
{project.projectGroupDesc || '暂无描述'} {project.projectGroupDesc || '暂无描述'}
</Text> </Text>
</div>
<div style={{
color: typeInfo.color,
fontSize: '24px',
opacity: 0.8,
marginLeft: '16px'
}}>
{Icon}
</div>
</div>
<div style={{ <div className="info-section">
display: 'flex', <div className="section-header">
flexDirection: 'column', <Text type="secondary"></Text>
gap: '16px', </div>
padding: '20px', <div className="section-content code">
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>
<Row gutter={16}> {project.environments && project.environments.length > 0 && (
<Col span={12}> <div className="info-section environment-section">
<Statistic <div className="section-header">
title={<Text type="secondary" style={{ fontSize: '13px' }}></Text>} <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
value={2} <Text type="secondary"></Text>
prefix={<EnvironmentOutlined />} <Tag color="blue" style={{ margin: 0, borderRadius: '10px' }}>
valueStyle={{ fontSize: '16px', color: typeInfo.color }} {project.environments.length}
/> </Tag>
</Col>
<Col span={12}>
<Statistic
title={<Text type="secondary" style={{ fontSize: '13px' }}></Text>}
value={5}
prefix={<TeamOutlined />}
valueStyle={{ fontSize: '16px', color: typeInfo.color }}
/>
</Col>
</Row>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<ClockCircleOutlined style={{ color: typeInfo.color }} />
<span>2</span>
</div> </div>
</div> <Tooltip title="查看更多">
<Button type="link" size="small" style={{ padding: 0 }}>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontSize: '14px',
padding: '4px 0',
}}>
{project.sort}
</div>
</div>
<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> </Button>
</Tooltip>
</div>
<div className="environment-list">
{project.environments.map(env => {
const buildType = buildTypeMap[env.buildType];
const deployType = deployTypeMap[env.deployType];
return (
<div key={env.id} className="environment-item">
<div className="env-header">
<div className="env-title">
<Text strong style={{ fontSize: '15px' }}>{env.envName}</Text>
<div className="env-tags">
<Tooltip title={`构建方式:${buildType.label}`}>
<Tag color={buildType.color} className="env-tag">
{buildType.icon}
</Tag>
</Tooltip>
<Tooltip title={`部署方式:${deployType.label}`}>
<Tag color={deployType.color} className="env-tag">
{deployType.icon}
</Tag>
</Tooltip>
</div>
</div>
<div className="env-meta">
<Tooltip title="环境编码">
<Tag className="env-code">
{env.envCode}
</Tag>
</Tooltip>
{env.envDesc && (
<Tooltip title={env.envDesc}>
<Text type="secondary" className="env-desc">
{env.envDesc}
</Text>
</Tooltip>
)}
</div>
</div>
<div className="env-footer">
<Space size={16}>
<Text type="secondary" style={{ fontSize: '12px' }}>
<ClockCircleOutlined style={{ marginRight: '4px' }} />
2
</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>
<RocketOutlined style={{ marginRight: '4px' }} />
12
</Text>
</Space> </Space>
</div> </div>
</div> </div>
);
})}
</div>
</div>
)}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '16px'
}}>
<Space>
<ClockCircleOutlined style={{ color: typeInfo.color }} />
<Text type="secondary">2</Text>
</Space>
<Text type="secondary">: {project.sort}</Text>
</div>
</div>
</Card> </Card>
); );
}; };
@ -307,9 +316,10 @@ const ProjectList: React.FC = () => {
onClick={handleAdd} onClick={handleAdd}
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
padding: '0 24px', padding: '0 24px',
height: '40px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
transition: 'all 0.3s ease',
}} }}
> >
@ -350,47 +360,39 @@ const ProjectList: React.FC = () => {
<div style={{ padding: '0 24px 24px' }}> <div style={{ padding: '0 24px 24px' }}>
<Row gutter={[24, 24]}> <Row gutter={[24, 24]}>
{filteredProjects.map(project => ( {filteredProjects.length > 0 ? (
<Col xs={24} sm={12} md={8} lg={6} key={project.id}> filteredProjects.map((project) => (
<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}
description={searchText ? "未找到匹配的项目组" : "暂无项目组数据"}
style={{ style={{
height: '100%', background: '#fff',
borderRadius: '16px', padding: '40px',
border: '1px dashed #d9d9d9', borderRadius: '8px',
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',
}} }}
> >
<div style={{ textAlign: 'center' }}> <Button type="primary" onClick={handleAdd}>
<PlusOutlined style={{ fontSize: '28px', color: '#8c8c8c' }} />
<div style={{ marginTop: '12px', color: '#8c8c8c', fontSize: '16px' }}></div> </Button>
</div> </Empty>
</Card>
</Col> </Col>
)}
</Row> </Row>
</div> </div>
<ProjectModal <ProjectGroupModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={fetchProjects} onSuccess={() => {
setModalVisible(false);
fetchProjects();
}}
initialValues={currentProject} initialValues={currentProject}
/> />
</PageContainer> </PageContainer>

View File

@ -1,4 +1,5 @@
import { BaseResponse, BaseRequest, BaseQuery } from '@/types/base'; import { BaseResponse, BaseRequest, BaseQuery } from '@/types/base';
import { Environment } from '../../Environment/List/types';
// 项目基础信息 // 项目基础信息
export interface ProjectGroup extends BaseResponse { export interface ProjectGroup extends BaseResponse {
@ -6,7 +7,8 @@ export interface ProjectGroup extends BaseResponse {
projectGroupCode: string; projectGroupCode: string;
projectGroupName: string; projectGroupName: string;
projectGroupDesc?: string; projectGroupDesc?: string;
projectGroupStatus: string; projectGroupStatus: 'ENABLE' | 'DISABLE';
environments: Environment[];
sort: number; sort: number;
} }