This commit is contained in:
dengqichen 2024-12-26 10:45:54 +08:00
parent 51aa8b6994
commit 09a6a020ce
3 changed files with 486 additions and 685 deletions

View File

@ -1,324 +1,247 @@
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, Tag, Space, Tooltip, Select } from 'antd'; import {Button, message, Popconfirm, Tag, Space, Tooltip, Select} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons'; import {PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined} from '@ant-design/icons';
import { getApplicationList, deleteApplication } from './service'; import {getApplicationPage, deleteApplication} from './service';
import { getProjectGroupList } from '../../ProjectGroup/List/service'; import {getProjectGroupList} from '../../ProjectGroup/List/service';
import type { Application } from './types'; import type {Application, ApplicationQuery} from './types';
import type { ProjectGroup } from '../../ProjectGroup/List/types'; import type {ProjectGroup} from '../../ProjectGroup/List/types';
import ApplicationModal from './components/ApplicationModal'; import ApplicationModal from './components/ApplicationModal';
import {ProTable} from '@ant-design/pro-components';
import type {ProColumns, ActionType} from '@ant-design/pro-components';
const { Title, Text } = Typography; const {Option} = Select;
const { Option } = Select;
const ApplicationList: React.FC = () => { const ApplicationList: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]); const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]); const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>(); const [modalVisible, setModalVisible] = useState(false);
const [loading, setLoading] = useState(false); const [currentApplication, setCurrentApplication] = useState<Application>();
const [modalVisible, setModalVisible] = useState(false); const actionRef = React.useRef<ActionType>();
const [currentApplication, setCurrentApplication] = useState<Application>();
// 获取项目列表 // 获取项目列表
const fetchProjects = async () => { const fetchProjects = async () => {
try { try {
const data = await getProjectGroupList(); const data = await getProjectGroupList();
setProjects(data); setProjects(data);
if (data.length > 0 && !selectedProjectGroupId) { if (data.length > 0 && !selectedProjectGroupId) {
setSelectedProjectGroupId(data[0].id); setSelectedProjectGroupId(data[0].id);
} }
} catch (error) { } catch (error) {
message.error('获取项目组列表失败'); message.error('获取项目组列表失败');
} }
}; };
// 获取应用列表 useEffect(() => {
const fetchApplications = async () => { fetchProjects();
if (!selectedProjectGroupId) return; }, []);
setLoading(true); const handleDelete = async (id: number) => {
try { try {
const data = await getApplicationList(); await deleteApplication(id);
// TODO: 这里需要根据selectedProjectId筛选应用 message.success('删除成功');
setApplications(data.filter(app => app.projectGroupId === selectedProjectGroupId)); actionRef.current?.reload();
} catch (error) { } catch (error) {
message.error('获取应用列表失败'); message.error('删除失败');
} finally { }
setLoading(false); };
}
};
useEffect(() => { const handleAdd = () => {
fetchProjects(); if (!selectedProjectGroupId) {
}, []); message.warning('请先选择项目组');
return;
}
setCurrentApplication(undefined);
setModalVisible(true);
};
useEffect(() => { const handleEdit = (application: Application) => {
fetchApplications(); setCurrentApplication(application);
}, [selectedProjectGroupId]); setModalVisible(true);
};
const handleDelete = async (id: number) => { const handleProjectChange = (value: number) => {
try { setSelectedProjectGroupId(value);
await deleteApplication(id); actionRef.current?.reload();
message.success('删除成功'); };
fetchApplications();
} catch (error) {
message.error('删除失败');
}
};
const handleAdd = () => { // 根据应用编码推测应用类型
setCurrentApplication(undefined); const getAppType = (appCode: string) => {
setModalVisible(true); if (appCode.toLowerCase().includes('web')) return {icon: <CodeOutlined/>, name: '前端应用', color: '#1890ff'};
}; if (appCode.toLowerCase().includes('api')) return {icon: <ApiOutlined/>, name: 'API服务', color: '#52c41a'};
return {icon: <CloudUploadOutlined/>, name: '后端服务', color: '#722ed1'};
};
const handleEdit = (application: Application) => { const columns: ProColumns<Application>[] = [
setCurrentApplication(application); {
setModalVisible(true); title: '应用编码',
}; dataIndex: 'appCode',
width: 180,
const handleProjectChange = (value: number) => { copyable: true,
setSelectedProjectGroupId(value); ellipsis: true,
}; fixed: 'left',
render: (text, record) => {
// 根据应用编码推测应用类型 const appType = getAppType(record.appCode);
const getAppType = (appCode: string) => { return (
if (appCode.toLowerCase().includes('web')) return { icon: <CodeOutlined />, name: '前端应用', color: '#1890ff' }; <Space>
if (appCode.toLowerCase().includes('api')) return { icon: <ApiOutlined />, name: 'API服务', color: '#52c41a' }; <Tooltip title={appType.name}>
return { icon: <CloudUploadOutlined />, name: '后端服务', color: '#722ed1' }; {appType.icon}
}; </Tooltip>
{text}
const ApplicationCard = ({ application }: { application: Application }) => { </Space>
const appType = getAppType(application.appCode); );
},
},
{
title: '应用名称',
dataIndex: 'appName',
width: 150,
ellipsis: true,
},
{
title: '应用描述',
dataIndex: 'appDesc',
ellipsis: true,
width: 200,
},
{
title: '应用状态',
dataIndex: 'appStatus',
width: 100,
valueEnum: {
'ENABLE': {text: '启用', status: 'Success'},
'DISABLE': {text: '禁用', status: 'Default'},
},
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true,
},
{
title: '操作',
width: 200,
key: 'action',
valueType: 'option',
fixed: 'right',
align: 'center',
render: (_, record) => [
<Button
key="edit"
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该应用吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
>
</Button>
</Popconfirm>,
<Button
key="deploy"
type="link"
icon={<CloudUploadOutlined/>}
>
</Button>
],
},
];
return ( return (
<Card <>
hoverable <ProTable<Application>
className="application-card" columns={columns}
style={{ actionRef={actionRef}
height: '100%', scroll={{x: 1300}}
borderRadius: '12px', cardBordered
overflow: 'hidden', request={async (params) => {
border: '1px solid #f0f0f0', if (!selectedProjectGroupId) {
transition: 'all 0.3s', return {
}} data: [],
bodyStyle={{ success: true,
padding: '24px', total: 0,
background: `linear-gradient(145deg, #ffffff 0%, ${appType.color}0A 100%)`, };
}} }
actions={[ const queryParams: ApplicationQuery = {
<Button pageSize: params.pageSize,
type="text" pageNum: params.current,
key="edit" projectGroupId: selectedProjectGroupId,
icon={<EditOutlined style={{ color: appType.color }} />} appCode: params.appCode as string,
onClick={() => handleEdit(application)} appName: params.appName as string,
> appStatus: params.appStatus as string,
};
</Button>, const data = await getApplicationPage(queryParams);
<Popconfirm return {
key="delete" data: data.content || [],
title="确定要删除该应用吗?" success: true,
onConfirm={() => handleDelete(application.id)} total: data.totalElements || 0,
> };
<Button }}
type="text" rowKey="id"
danger search={{
icon={<DeleteOutlined />} labelWidth: 'auto',
> span: {
xs: 24,
</Button> sm: 12,
</Popconfirm>, md: 8,
]} lg: 6,
> xl: 6,
<div style={{ position: 'relative' }}> xxl: 6,
<div style={{ },
position: 'absolute', }}
top: '-12px', options={{
right: '-12px', setting: {
padding: '4px 8px', listsHeight: 400,
background: appType.color, },
color: '#fff', }}
borderRadius: '0 0 0 8px', form={{
fontSize: '12px', syncToUrl: true,
display: 'flex', }}
alignItems: 'center', pagination={{
gap: '4px' pageSize: 10,
}}> showQuickJumper: true,
{appType.icon} }}
{appType.name} dateFormatter="string"
</div> toolBarRender={() => [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
disabled={!selectedProjectGroupId}
>
</Button>,
]}
/>
<Title level={4} style={{ {selectedProjectGroupId && (
marginBottom: '16px', <ApplicationModal
color: '#1f1f1f', visible={modalVisible}
display: 'flex', onCancel={() => setModalVisible(false)}
alignItems: 'center', onSuccess={() => {
gap: '8px' setModalVisible(false);
}}> actionRef.current?.reload();
{application.appName} }}
<Tag initialValues={currentApplication}
color={application.appStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'} projectGroupId={selectedProjectGroupId}
style={{ />
borderRadius: '4px', )}
fontSize: '12px', </>
padding: '0 8px',
}}
>
{application.appStatus === 'ENABLE' ? '启用' : '禁用'}
</Tag>
</Title>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '20px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{application.appDesc || '暂无描述'}
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
padding: '16px',
background: '#fff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
fontFamily: 'monospace',
marginTop: '4px',
fontSize: '14px',
padding: '4px 8px',
background: '#f5f5f5',
borderRadius: '4px',
}}>
{application.appCode}
</div>
</div>
<Space size="large">
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
textAlign: 'center',
marginTop: '4px',
fontSize: '14px',
}}>
{application.sort}
</div>
</div>
<Tooltip title="更多功能开发中...">
<Button type="text" size="small"></Button>
</Tooltip>
</Space>
</div>
</div>
</Card>
); );
};
const currentProject = projectGroups.find(p => p.id === selectedProjectGroupId);
return (
<PageContainer
header={{
title: '应用管理',
subTitle: (
<Space>
<span></span>
<Select
value={selectedProjectGroupId}
onChange={handleProjectChange}
style={{ width: 200 }}
placeholder="请选择项目组"
showSearch
optionFilterProp="children"
>
{projectGroups.map(project => (
<Option key={project.id} value={project.id}>
{project.projectGroupName}
</Option>
))}
</Select>
</Space>
),
extra: [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '6px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
}}
disabled={!selectedProjectGroupId}
>
</Button>
],
}}
>
<div style={{ padding: '24px' }}>
<Row gutter={[24, 24]}>
{applications.map(application => (
<Col xs={24} sm={12} md={8} lg={6} key={application.id}>
<ApplicationCard application={application} />
</Col>
))}
{selectedProjectGroupId && (
<Col xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
onClick={handleAdd}
style={{
height: '100%',
borderRadius: '12px',
border: '1px dashed #d9d9d9',
background: '#fafafa',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '250px',
}}
bodyStyle={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ textAlign: 'center' }}>
<PlusOutlined style={{ fontSize: '24px', color: '#8c8c8c' }} />
<div style={{ marginTop: '8px', color: '#8c8c8c' }}></div>
</div>
</Card>
</Col>
)}
</Row>
</div>
<ApplicationModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={fetchApplications}
initialValues={currentApplication}
projectGroupId={selectedProjectGroupId!}
/>
</PageContainer>
);
}; };
export default ApplicationList; export default ApplicationList;

View File

@ -5,8 +5,8 @@ import {PlusOutlined, DeleteOutlined, EditOutlined, SearchOutlined, ReloadOutlin
import {getEnvironmentList, deleteEnvironment, getEnvironmentPage} from './service'; import {getEnvironmentList, deleteEnvironment, getEnvironmentPage} from './service';
import type {Environment} from './types'; import type {Environment} from './types';
import EnvironmentModal from './components/EnvironmentModal'; import EnvironmentModal from './components/EnvironmentModal';
import { ProTable } from '@ant-design/pro-components'; import {ProTable} from '@ant-design/pro-components';
import type { ProColumns } from '@ant-design/pro-components'; import type {ProColumns} from '@ant-design/pro-components';
const EnvironmentList: React.FC = () => { const EnvironmentList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
@ -104,71 +104,70 @@ const EnvironmentList: React.FC = () => {
]; ];
return ( return (
<>
<ProTable<Environment>
columns={columns}
actionRef={actionRef}
cardBordered
request={async (params) => {
const {current, pageSize} = params;
const data = await getEnvironmentPage({
current,
pageSize,
});
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
span: {
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
xxl: 6,
},
}}
options={{
setting: {
listsHeight: 400,
},
}}
form={{
syncToUrl: true,
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
dateFormatter="string"
toolBarRender={() => [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
>
</Button>,
]}
/>
<> <EnvironmentModal
<ProTable<Environment> visible={modalVisible}
columns={columns} onCancel={() => setModalVisible(false)}
actionRef={actionRef} onSuccess={() => {
cardBordered setModalVisible(false);
request={async (params) => { actionRef.current?.reload();
const { current, pageSize } = params; }}
const data = await getEnvironmentPage({ initialValues={currentEnvironment}
current, />
pageSize, </>
});
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
span: {
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
xxl: 6,
},
}}
options={{
setting: {
listsHeight: 400,
},
}}
form={{
syncToUrl: true,
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
dateFormatter="string"
toolBarRender={() => [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
>
</Button>,
]}
/>
<EnvironmentModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={() => {
setModalVisible(false);
actionRef.current?.reload();
}}
initialValues={currentEnvironment}
/>
</>
); );
}; };

View File

@ -1,52 +1,31 @@
import React, {useState, useEffect} from 'react'; import React, {useState} 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 {Button, message, Popconfirm, Space, Tag, Tooltip} from 'antd';
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
TeamOutlined, TeamOutlined,
SearchOutlined,
EnvironmentOutlined, EnvironmentOutlined,
RocketOutlined RocketOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import {getProjectGroupList, deleteProjectGroup} from './service'; import {getProjectGroupPage, deleteProjectGroup} from './service';
import type {ProjectGroup} from './types'; import type {ProjectGroup, ProjectGroupQueryParams} from './types';
import ProjectGroupModal from './components/ProjectGroupModal'; import ProjectGroupModal from './components/ProjectGroupModal';
import { ProjectGroupTypeEnum } from './types'; import { ProjectGroupTypeEnum } from './types';
import { ProTable } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
const {Title, Text} = Typography; const ProjectGroupList: React.FC = () => {
const {Search} = Input;
const ProjectList: React.FC = () => {
const [projects, setProjects] = useState<ProjectGroup[]>([]);
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 actionRef = React.useRef<ActionType>();
const [projectType, setProjectType] = useState<string>('ALL');
const fetchProjects = async () => {
setLoading(true);
try {
const data = await getProjectGroupList();
setProjects(data);
} catch (error) {
message.error('获取项目组列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
await deleteProjectGroup(id); await deleteProjectGroup(id);
message.success('删除成功'); message.success('删除成功');
fetchProjects(); actionRef.current?.reload();
} catch (error) { } catch (error) {
message.error('删除失败'); message.error('删除失败');
} }
@ -63,305 +42,205 @@ const ProjectList: React.FC = () => {
}; };
const getProjectTypeInfo = (type: ProjectGroupTypeEnum) => { const getProjectTypeInfo = (type: ProjectGroupTypeEnum) => {
switch (type) { switch (type) {
case ProjectGroupTypeEnum.PRODUCT: case ProjectGroupTypeEnum.PRODUCT:
return { return {
type: ProjectGroupTypeEnum.PRODUCT, type: ProjectGroupTypeEnum.PRODUCT,
label: '产品型', label: '产品型',
color: '#1890ff', color: '#1890ff',
icon: <RocketOutlined/>, icon: <RocketOutlined/>,
description: '产品型相关的项目组' description: '产品型相关的项目组'
}; };
case ProjectGroupTypeEnum.PROJECT: case ProjectGroupTypeEnum.PROJECT:
return { return {
type: ProjectGroupTypeEnum.PROJECT, type: ProjectGroupTypeEnum.PROJECT,
label: '项目型', label: '项目型',
color: '#52c41a', color: '#52c41a',
icon: <EnvironmentOutlined/>, icon: <EnvironmentOutlined/>,
description: '项目型相关的项目组' description: '项目型相关的项目组'
}; };
default: default:
return { return {
type: ProjectGroupTypeEnum.PROJECT, type: ProjectGroupTypeEnum.PROJECT,
label: '项目型', label: '项目型',
color: '#722ed1', color: '#722ed1',
icon: <TeamOutlined/>, icon: <TeamOutlined/>,
description: '项目型相关的项目组' description: '项目型相关的项目组'
}; };
} }
};
const filteredProjects = projects.filter(project => {
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.type).type === projectType;
return matchesSearch && matchesType;
});
const ProjectCard = ({projectGroup}: { projectGroup: ProjectGroup }) => {
const typeInfo = getProjectTypeInfo(projectGroup.type);
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'
}}>
{projectGroup.projectGroupName}({projectGroup.projectGroupCode})
<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',
}}
>
{projectGroup.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)',
}}>
<Row gutter={16}>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
value={projectGroup.environments.length}
prefix={<EnvironmentOutlined/>}
valueStyle={{fontSize: '16px', color: typeInfo.color}}
/>
</Col>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
value={projectGroup.applications.length}
prefix={<TeamOutlined/>}
valueStyle={{fontSize: '16px', color: typeInfo.color}}
/>
</Col>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
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(projectGroup)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
>
</Button>
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(projectGroup.id)}
>
<Button
danger
icon={<DeleteOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Popconfirm>
<Button
icon={<TeamOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Space>
</div>
</div>
</Card>
);
}; };
const columns: ProColumns<ProjectGroup>[] = [
{
title: '项目组编码',
dataIndex: 'projectGroupCode',
width: 120,
copyable: true,
ellipsis: true,
fixed: 'left',
},
{
title: '项目组名称',
dataIndex: 'projectGroupName',
width: 150,
ellipsis: true,
},
{
title: '项目组描述',
dataIndex: 'projectGroupDesc',
ellipsis: true,
},
{
title: '项目组类型',
dataIndex: 'type',
width: 100,
render: (type) => {
const typeInfo = getProjectTypeInfo(type as ProjectGroupTypeEnum);
return (
<Tag color={typeInfo.color}>
{typeInfo.label}
</Tag>
);
},
valueEnum: {
[ProjectGroupTypeEnum.PRODUCT]: { text: '产品型' },
[ProjectGroupTypeEnum.PROJECT]: { text: '项目型' },
},
},
{
title: '环境数量',
dataIndex: 'environments',
width: 100,
render: (_, record) => (
<Tooltip title="环境数量">
<Space>
<EnvironmentOutlined />
{record.environments?.length || 0}
</Space>
</Tooltip>
),
},
{
title: '项目数量',
dataIndex: 'applications',
width: 100,
render: (_, record) => (
<Tooltip title="项目数量">
<Space>
<TeamOutlined />
{record.applications?.length || 0}
</Space>
</Tooltip>
),
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true,
},
{
title: '操作',
width: 180,
key: 'action',
valueType: 'option',
fixed: 'right',
render: (_, record) => (
<Space>
<Button
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
>
</Button>
</Popconfirm>
</Space>
),
},
];
return ( return (
<PageContainer <>
header={{ <ProTable<ProjectGroup>
title: '项目组管理', columns={columns}
extra: [ actionRef={actionRef}
cardBordered
request={async (params) => {
const queryParams: ProjectGroupQueryParams = {
pageSize: params.pageSize,
pageNum: params.current,
projectGroupName: params.projectGroupName as string,
projectGroupCode: params.projectGroupCode as string,
projectGroupStatus: params.projectGroupStatus as string,
};
const data = await getProjectGroupPage(queryParams);
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
span: {
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
xxl: 6,
},
}}
options={{
setting: {
listsHeight: 400,
},
}}
form={{
syncToUrl: true,
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
dateFormatter="string"
toolBarRender={() => [
<Button <Button
key="add" key="add"
type="primary" type="primary"
icon={<PlusOutlined/>}
onClick={handleAdd} onClick={handleAdd}
style={{ icon={<PlusOutlined/>}
borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
padding: '0 24px',
}}
> >
</Button> </Button>,
], ]}
}} />
>
<div style={{marginBottom: '24px', padding: '0 24px'}}>
<Row gutter={16} align="middle">
<Col flex="300px">
<Search
placeholder="搜索项目组名称、编码或描述"
allowClear
enterButton={<><SearchOutlined/> </>}
size="large"
value={searchText}
onChange={e => setSearchText(e.target.value)}
style={{width: '100%'}}
/>
</Col>
<Col>
<Select
placeholder="项目组类型"
style={{ width: 200 }}
value={projectType}
onChange={setProjectType}
options={[
{ label: '全部类型', value: 'ALL' },
{ label: '产品项目组', value: ProjectGroupTypeEnum.PRODUCT },
{ label: '项目项目组', value: ProjectGroupTypeEnum.PROJECT }
]}
size="large"
/>
</Col>
</Row>
</div>
<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 projectGroup={project}/>
</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>
<ProjectGroupModal <ProjectGroupModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={fetchProjects} onSuccess={() => {
setModalVisible(false);
actionRef.current?.reload();
}}
initialValues={currentProject} initialValues={currentProject}
/> />
</PageContainer> </>
); );
}; };
export default ProjectList; export default ProjectGroupList;