1
This commit is contained in:
parent
3f1240e972
commit
b9d559a683
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user