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

View File

@ -5,14 +5,14 @@ import type { Environment } from '../../../Environment/List/types';
import { createProjectGroup, updateProjectGroup } from '../service';
import { getEnvironmentList } from '../../../Environment/List/service';
interface ProjectModalProps {
interface ProjectGroupModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: ProjectGroup;
}
const ProjectModal: React.FC<ProjectModalProps> = ({
const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
visible,
onCancel,
onSuccess,
@ -83,6 +83,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
onOk={handleSubmit}
onCancel={handleCancel}
destroyOnClose
width={560}
>
<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 { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
SearchOutlined,
EnvironmentOutlined,
ClockCircleOutlined,
RocketOutlined
} from '@ant-design/icons';
import { 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 { getProjectGroupList, deleteProjectGroup } from './service';
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 { Search } = Input;
@ -91,204 +83,221 @@ 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={{
height: '100%',
borderRadius: '16px',
borderRadius: '12px',
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%)`,
transition: 'all 0.3s ease',
}}
actions={[
<Tooltip title="编辑项目组">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(project)}
>
</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={{ flex: 1 }}>
<div style={{ position: 'relative', padding: '16px' }}>
<div style={{
display: 'flex',
alignItems: 'flex-start',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px',
position: 'relative'
marginBottom: '16px'
}}>
<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>
<Title level={4} style={{ margin: 0 }}>
{project.projectGroupName}
</Title>
<Tooltip title={typeInfo.description}>
<Tag color={typeInfo.color} style={{ borderRadius: '12px', padding: '0 12px' }}>
{typeInfo.icon} {typeInfo.label}
</Tag>
</Tooltip>
</div>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '24px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{project.projectGroupDesc || '暂无描述'}
</Text>
<div className="info-section">
<div className="section-header">
<Text type="secondary"></Text>
</div>
<div style={{
color: typeInfo.color,
fontSize: '24px',
opacity: 0.8,
marginLeft: '16px'
}}>
{Icon}
<div className="section-content code">
{project.projectGroupCode}
</div>
</div>
{project.environments && project.environments.length > 0 && (
<div className="info-section environment-section">
<div className="section-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Text type="secondary"></Text>
<Tag color="blue" style={{ margin: 0, borderRadius: '10px' }}>
{project.environments.length}
</Tag>
</div>
<Tooltip title="查看更多">
<Button type="link" size="small" style={{ padding: 0 }}>
</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>
</div>
</div>
);
})}
</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)',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '16px'
}}>
<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={12}>
<Statistic
title={<Text type="secondary" style={{ fontSize: '13px' }}></Text>}
value={2}
prefix={<EnvironmentOutlined />}
valueStyle={{ fontSize: '16px', color: typeInfo.color }}
/>
</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>
<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>
<Space>
<ClockCircleOutlined style={{ color: typeInfo.color }} />
<Text type="secondary">2</Text>
</Space>
<Text type="secondary">: {project.sort}</Text>
</div>
</div>
</Card>
@ -307,9 +316,10 @@ const ProjectList: React.FC = () => {
onClick={handleAdd}
style={{
borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
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' }}>
<Row gutter={[24, 24]}>
{filteredProjects.map(project => (
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
<ProjectCard project={project} />
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<Col key={project.id} xs={24} sm={12} md={8} lg={6}>
<ProjectCard project={project} />
</Col>
))
) : (
<Col span={24}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={searchText ? "未找到匹配的项目组" : "暂无项目组数据"}
style={{
background: '#fff',
padding: '40px',
borderRadius: '8px',
}}
>
<Button type="primary" onClick={handleAdd}>
</Button>
</Empty>
</Col>
))}
<Col xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
onClick={handleAdd}
style={{
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',
}}
>
<div style={{ textAlign: 'center' }}>
<PlusOutlined style={{ fontSize: '28px', color: '#8c8c8c' }} />
<div style={{ marginTop: '12px', color: '#8c8c8c', fontSize: '16px' }}></div>
</div>
</Card>
</Col>
)}
</Row>
</div>
<ProjectModal
<ProjectGroupModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={fetchProjects}
onSuccess={() => {
setModalVisible(false);
fetchProjects();
}}
initialValues={currentProject}
/>
</PageContainer>

View File

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