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,23 +1,23 @@
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 [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [currentApplication, setCurrentApplication] = useState<Application>(); const [currentApplication, setCurrentApplication] = useState<Application>();
const actionRef = React.useRef<ActionType>();
// 获取项目列表 // 获取项目列表
const fetchProjects = async () => { const fetchProjects = async () => {
@ -32,41 +32,25 @@ const ApplicationList: React.FC = () => {
} }
}; };
// 获取应用列表
const fetchApplications = async () => {
if (!selectedProjectGroupId) return;
setLoading(true);
try {
const data = await getApplicationList();
// TODO: 这里需要根据selectedProjectId筛选应用
setApplications(data.filter(app => app.projectGroupId === selectedProjectGroupId));
} catch (error) {
message.error('获取应用列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
fetchProjects(); fetchProjects();
}, []); }, []);
useEffect(() => {
fetchApplications();
}, [selectedProjectGroupId]);
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
await deleteApplication(id); await deleteApplication(id);
message.success('删除成功'); message.success('删除成功');
fetchApplications(); actionRef.current?.reload();
} catch (error) { } catch (error) {
message.error('删除失败'); message.error('删除失败');
} }
}; };
const handleAdd = () => { const handleAdd = () => {
if (!selectedProjectGroupId) {
message.warning('请先选择项目组');
return;
}
setCurrentApplication(undefined); setCurrentApplication(undefined);
setModalVisible(true); setModalVisible(true);
}; };
@ -78,246 +62,185 @@ const ApplicationList: React.FC = () => {
const handleProjectChange = (value: number) => { const handleProjectChange = (value: number) => {
setSelectedProjectGroupId(value); setSelectedProjectGroupId(value);
actionRef.current?.reload();
}; };
// 根据应用编码推测应用类型 // 根据应用编码推测应用类型
const getAppType = (appCode: string) => { const getAppType = (appCode: string) => {
if (appCode.toLowerCase().includes('web')) return { icon: <CodeOutlined />, name: '前端应用', color: '#1890ff' }; if (appCode.toLowerCase().includes('web')) return {icon: <CodeOutlined/>, name: '前端应用', color: '#1890ff'};
if (appCode.toLowerCase().includes('api')) return { icon: <ApiOutlined />, name: 'API服务', color: '#52c41a' }; if (appCode.toLowerCase().includes('api')) return {icon: <ApiOutlined/>, name: 'API服务', color: '#52c41a'};
return { icon: <CloudUploadOutlined />, name: '后端服务', color: '#722ed1' }; return {icon: <CloudUploadOutlined/>, name: '后端服务', color: '#722ed1'};
}; };
const ApplicationCard = ({ application }: { application: Application }) => { const columns: ProColumns<Application>[] = [
const appType = getAppType(application.appCode); {
title: '应用编码',
dataIndex: 'appCode',
width: 180,
copyable: true,
ellipsis: true,
fixed: 'left',
render: (text, record) => {
const appType = getAppType(record.appCode);
return ( return (
<Card <Space>
hoverable <Tooltip title={appType.name}>
className="application-card" {appType.icon}
style={{ </Tooltip>
height: '100%', {text}
borderRadius: '12px', </Space>
overflow: 'hidden', );
border: '1px solid #f0f0f0', },
transition: 'all 0.3s', },
}} {
bodyStyle={{ title: '应用名称',
padding: '24px', dataIndex: 'appName',
background: `linear-gradient(145deg, #ffffff 0%, ${appType.color}0A 100%)`, width: 150,
}} ellipsis: true,
actions={[ },
{
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 <Button
type="text"
key="edit" key="edit"
icon={<EditOutlined style={{ color: appType.color }} />} type="link"
onClick={() => handleEdit(application)} icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
> >
</Button>, </Button>,
<Popconfirm <Popconfirm
key="delete" key="delete"
title="确定要删除该应用吗?" title="确定要删除该应用吗?"
onConfirm={() => handleDelete(application.id)} description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
> >
<Button <Button
type="text" type="link"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined/>}
> >
</Button> </Button>
</Popconfirm>, </Popconfirm>,
]} <Button
key="deploy"
type="link"
icon={<CloudUploadOutlined/>}
> >
<div style={{ position: 'relative' }}>
<div style={{ </Button>
position: 'absolute', ],
top: '-12px', },
right: '-12px', ];
padding: '4px 8px',
background: appType.color,
color: '#fff',
borderRadius: '0 0 0 8px',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
{appType.icon}
{appType.name}
</div>
<Title level={4} style={{
marginBottom: '16px',
color: '#1f1f1f',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{application.appName}
<Tag
color={application.appStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
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 ( return (
<PageContainer <>
header={{ <ProTable<Application>
title: '应用管理', columns={columns}
subTitle: ( actionRef={actionRef}
<Space> scroll={{x: 1300}}
<span></span> cardBordered
<Select request={async (params) => {
value={selectedProjectGroupId} if (!selectedProjectGroupId) {
onChange={handleProjectChange} return {
style={{ width: 200 }} data: [],
placeholder="请选择项目组" success: true,
showSearch total: 0,
optionFilterProp="children" };
> }
{projectGroups.map(project => ( const queryParams: ApplicationQuery = {
<Option key={project.id} value={project.id}> pageSize: params.pageSize,
{project.projectGroupName} pageNum: params.current,
</Option> projectGroupId: selectedProjectGroupId,
))} appCode: params.appCode as string,
</Select> appName: params.appName as string,
</Space> appStatus: params.appStatus as string,
), };
extra: [ const data = await getApplicationPage(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: '6px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
}}
disabled={!selectedProjectGroupId} disabled={!selectedProjectGroupId}
> >
</Button> </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>
{selectedProjectGroupId && (
<ApplicationModal <ApplicationModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={fetchApplications} onSuccess={() => {
setModalVisible(false);
actionRef.current?.reload();
}}
initialValues={currentApplication} initialValues={currentApplication}
projectGroupId={selectedProjectGroupId!} projectGroupId={selectedProjectGroupId}
/> />
</PageContainer> )}
</>
); );
}; };

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,14 +104,13 @@ const EnvironmentList: React.FC = () => {
]; ];
return ( return (
<> <>
<ProTable<Environment> <ProTable<Environment>
columns={columns} columns={columns}
actionRef={actionRef} actionRef={actionRef}
cardBordered cardBordered
request={async (params) => { request={async (params) => {
const { current, pageSize } = params; const {current, pageSize} = params;
const data = await getEnvironmentPage({ const data = await getEnvironmentPage({
current, current,
pageSize, pageSize,

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('删除失败');
} }
@ -91,277 +70,177 @@ const ProjectList: React.FC = () => {
} }
}; };
const filteredProjects = projects.filter(project => { const columns: ProColumns<ProjectGroup>[] = [
const matchesSearch = searchText ? ( {
project.projectGroupName.toLowerCase().includes(searchText.toLowerCase()) || title: '项目组编码',
project.projectGroupCode.toLowerCase().includes(searchText.toLowerCase()) || dataIndex: 'projectGroupCode',
project.projectGroupDesc?.toLowerCase().includes(searchText.toLowerCase()) width: 120,
) : true; copyable: true,
ellipsis: true,
const matchesType = projectType === 'ALL' ? true : fixed: 'left',
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={{ title: '项目组名称',
padding: '24px', dataIndex: 'projectGroupName',
height: '100%', width: 150,
display: 'flex', ellipsis: true,
flexDirection: 'column', },
background: `linear-gradient(145deg, #ffffff 0%, ${typeInfo.color}08 100%)`, {
}} title: '项目组描述',
> dataIndex: 'projectGroupDesc',
<div style={{flex: 1}}> ellipsis: true,
<div style={{ },
display: 'flex', {
alignItems: 'flex-start', title: '项目组类型',
justifyContent: 'space-between', dataIndex: 'type',
marginBottom: '16px', width: 100,
position: 'relative' render: (type) => {
}}> const typeInfo = getProjectTypeInfo(type as ProjectGroupTypeEnum);
<div style={{flex: 1}}> return (
<Title level={4} style={{ <Tag color={typeInfo.color}>
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} {typeInfo.label}
</Tag> </Tag>
</Title> );
<Text },
type="secondary" valueEnum: {
style={{ [ProjectGroupTypeEnum.PRODUCT]: { text: '产品型' },
display: 'block', [ProjectGroupTypeEnum.PROJECT]: { text: '项目型' },
marginTop: '8px', },
height: '40px', },
overflow: 'hidden', {
textOverflow: 'ellipsis', title: '环境数量',
WebkitLineClamp: 2, dataIndex: 'environments',
WebkitBoxOrient: 'vertical', width: 100,
display: '-webkit-box', render: (_, record) => (
}} <Tooltip title="环境数量">
> <Space>
{projectGroup.projectGroupDesc || '暂无描述'} <EnvironmentOutlined />
</Text> {record.environments?.length || 0}
</div> </Space>
<div style={{ </Tooltip>
color: typeInfo.color, ),
fontSize: '24px', },
opacity: 0.8, {
marginLeft: '16px' title: '项目数量',
}}> dataIndex: 'applications',
{Icon} width: 100,
</div> render: (_, record) => (
</div> <Tooltip title="项目数量">
<Space>
<div style={{ <TeamOutlined />
display: 'flex', {record.applications?.length || 0}
flexDirection: 'column', </Space>
gap: '16px', </Tooltip>
padding: '20px', ),
background: '#fff', },
borderRadius: '12px', {
boxShadow: '0 2px 8px rgba(0,0,0,0.04)', title: '排序',
}}> dataIndex: 'sort',
<Row gutter={16}> width: 80,
<Col span={8}> sorter: true,
<Statistic },
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>} {
value={projectGroup.environments.length} title: '操作',
prefix={<EnvironmentOutlined/>} width: 180,
valueStyle={{fontSize: '16px', color: typeInfo.color}} key: 'action',
/> valueType: 'option',
</Col> fixed: 'right',
<Col span={8}> render: (_, record) => (
<Statistic <Space>
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 <Button
type="primary" type="link"
icon={<EditOutlined/>} icon={<EditOutlined/>}
onClick={() => handleEdit(projectGroup)} onClick={() => handleEdit(record)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
> >
</Button> </Button>
<Popconfirm <Popconfirm
title="确定要删除该项目组吗?" title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作" description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(projectGroup.id)} onConfirm={() => handleDelete(record.id)}
> >
<Button <Button
type="link"
danger danger
icon={<DeleteOutlined/>} icon={<DeleteOutlined/>}
style={{borderRadius: '6px'}}
> >
</Button> </Button>
</Popconfirm> </Popconfirm>
<Button
icon={<TeamOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Space> </Space>
</div> ),
</div> },
</Card> ];
);
};
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;