This commit is contained in:
dengqichen 2024-12-23 17:09:32 +08:00
parent cf0b5589d4
commit 434d21d501
13 changed files with 911 additions and 605 deletions

View File

@ -8,7 +8,7 @@ interface ApplicationModalProps {
onCancel: () => void;
onSuccess: () => void;
initialValues?: Application;
projectId: number;
projectGroupId: number;
}
const ApplicationModal: React.FC<ApplicationModalProps> = ({
@ -16,7 +16,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
onCancel,
onSuccess,
initialValues,
projectId,
projectGroupId,
}) => {
const [form] = Form.useForm();
const isEdit = !!initialValues;
@ -26,19 +26,28 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
if (initialValues) {
form.setFieldsValue(initialValues);
} else {
form.setFieldsValue({ projectId, appStatus: 'ENABLE', sort: 0 });
form.setFieldsValue({
appStatus: 'ENABLE',
sort: 0,
projectGroupId: projectGroupId // 设置项目组ID的初始值
});
}
}
}, [visible, initialValues, form, projectId]);
}, [visible, initialValues, form, projectGroupId]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const submitData = {
...values,
projectGroupId: projectGroupId // 确保提交时包含项目组ID
};
if (isEdit) {
await updateApplication({ ...values, id: initialValues.id });
await updateApplication({ ...submitData, id: initialValues?.id });
message.success('更新成功');
} else {
await createApplication(values);
await createApplication(submitData);
message.success('创建成功');
}
onSuccess();
@ -67,6 +76,11 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
layout="vertical"
initialValues={{ appStatus: 'ENABLE', sort: 0 }}
>
{/* 添加一个隐藏的表单项来存储项目组ID */}
<Form.Item name="projectGroupId" hidden>
<Input />
</Form.Item>
<Form.Item
name="appCode"
label="应用编码"
@ -75,7 +89,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
{ pattern: /^[A-Za-z0-9_-]+$/, message: '应用编码只能包含字母、数字、下划线和连字符' }
]}
>
<Input placeholder="请输入应用编码" />
<Input placeholder="请输入应用编码" disabled={isEdit} />
</Form.Item>
<Form.Item
@ -111,10 +125,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="projectId" hidden>
<Input />
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,27 +1,46 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Tooltip, Select } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons';
import { getApplicationList, deleteApplication } from './service';
import { getProjectGroupList } from '../../ProjectGroup/List/service';
import type { Application } from './types';
import type { ProjectGroup } from '../../ProjectGroup/List/types';
import ApplicationModal from './components/ApplicationModal';
const { Title, Text } = Typography;
const { Option } = Select;
const ApplicationList: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentApplication, setCurrentApplication] = useState<Application>();
// TODO: 这里需要从上下文或者URL参数获取projectId
const projectId = 1;
// 获取项目列表
const fetchProjects = async () => {
try {
const data = await getProjectGroupList();
setProjects(data);
if (data.length > 0 && !selectedProjectGroupId) {
setSelectedProjectGroupId(data[0].id);
}
} catch (error) {
message.error('获取项目组列表失败');
}
};
// 获取应用列表
const fetchApplications = async () => {
if (!selectedProjectGroupId) return;
setLoading(true);
try {
const data = await getApplicationList();
setApplications(data);
// TODO: 这里需要根据selectedProjectId筛选应用
setApplications(data.filter(app => app.projectId === selectedProjectGroupId));
} catch (error) {
message.error('获取应用列表失败');
} finally {
@ -29,10 +48,14 @@ const ApplicationList: React.FC = () => {
}
};
React.useEffect(() => {
fetchApplications();
useEffect(() => {
fetchProjects();
}, []);
useEffect(() => {
fetchApplications();
}, [selectedProjectGroupId]);
const handleDelete = async (id: number) => {
try {
await deleteApplication(id);
@ -53,6 +76,10 @@ const ApplicationList: React.FC = () => {
setModalVisible(true);
};
const handleProjectChange = (value: number) => {
setSelectedProjectGroupId(value);
};
// 根据应用编码推测应用类型
const getAppType = (appCode: string) => {
if (appCode.toLowerCase().includes('web')) return { icon: <CodeOutlined />, name: '前端应用', color: '#1890ff' };
@ -201,10 +228,31 @@ const ApplicationList: React.FC = () => {
);
};
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"
@ -215,6 +263,7 @@ const ApplicationList: React.FC = () => {
borderRadius: '6px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
}}
disabled={!selectedProjectGroupId}
>
</Button>
@ -228,6 +277,7 @@ const ApplicationList: React.FC = () => {
<ApplicationCard application={application} />
</Col>
))}
{selectedProjectGroupId && (
<Col xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
@ -256,6 +306,7 @@ const ApplicationList: React.FC = () => {
</div>
</Card>
</Col>
)}
</Row>
</div>
@ -264,7 +315,7 @@ const ApplicationList: React.FC = () => {
onCancel={() => setModalVisible(false)}
onSuccess={fetchApplications}
initialValues={currentApplication}
projectId={projectId}
projectGroupId={selectedProjectGroupId!}
/>
</PageContainer>
);

View File

@ -1,43 +1,55 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, message } from 'antd';
import type { Environment } from '../types';
import { createEnvironment, updateEnvironment } from '../service';
import React, {useEffect, useState} from 'react';
import {Modal, Form, Input, Select, message} from 'antd';
import type {Environment} from '../types';
import {BuildTypeEnum, DeployTypeEnum} from '../types';
import {createEnvironment, updateEnvironment} from '../service';
interface EnvironmentModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: Environment;
projectId: number;
tenantCode: string;
}
const {Option} = Select;
const buildTypeOptions = [
{label: 'Jenkins构建', value: BuildTypeEnum.JENKINS},
{label: 'GitLab Runner构建', value: BuildTypeEnum.GITLAB_RUNNER},
{label: 'GitHub Action构建', value: BuildTypeEnum.GITHUB_ACTION},
];
const deployTypeOptions = [
{label: 'Kubernetes集群部署', value: DeployTypeEnum.K8S},
{label: 'Docker容器部署', value: DeployTypeEnum.DOCKER},
{label: '虚拟机部署', value: DeployTypeEnum.VM},
];
const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
visible,
onCancel,
onSuccess,
initialValues,
projectId,
tenantCode,
}) => {
}) => {
const [form] = Form.useForm();
const isEdit = !!initialValues;
const [loading, setLoading] = useState(false);
useEffect(() => {
if (visible) {
if (initialValues) {
form.setFieldsValue(initialValues);
} else {
form.setFieldsValue({ projectId, tenantCode, sort: 0 });
form.setFieldsValue(initialValues || {});
}
}
}, [visible, initialValues, form, projectId, tenantCode]);
}, [visible, initialValues]);
const handleSubmit = async () => {
const handleOk = async () => {
try {
const values = await form.validateFields();
if (isEdit) {
await updateEnvironment({ ...values, id: initialValues.id });
setLoading(true);
if (initialValues?.id) {
await updateEnvironment({
...values,
id: initialValues.id,
});
message.success('更新成功');
} else {
await createEnvironment(values);
@ -45,70 +57,98 @@ const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
console.error('Submit failed:', error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={isEdit ? '编辑环境' : '新建环境'}
title={initialValues ? '编辑环境' : '新建环境'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={loading}
destroyOnClose
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={{ sort: 0 }}
initialValues={initialValues || {sort: 0}}
>
<Form.Item
name="envName"
label="环境名称"
rules={[
{required: true, message: '请输入环境名称'},
{max: 50, message: '环境名称最多50个字符'}
]}
>
<Input placeholder="请输入环境名称"/>
</Form.Item>
<Form.Item
name="envCode"
label="环境编码"
rules={[
{ required: true, message: '请输入环境编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '环境编码只能包含字母、数字、下划线和连字符' }
{required: true, message: '请输入环境编码'},
{max: 50, message: '环境编码最多50个字符'},
{
pattern: /^[A-Za-z0-9_-]+$/,
message: '环境编码只能包含字母、数字、下划线和连字符'
}
]}
>
<Input placeholder="请输入环境编码" />
<Input placeholder="请输入环境编码"/>
</Form.Item>
<Form.Item
name="envName"
label="环境名称"
rules={[{ required: true, message: '请输入环境名称' }]}
name="buildType"
label="构建方式"
rules={[{required: true, message: '请选择构建方式'}]}
>
<Input placeholder="请输入环境名称" />
<Select
placeholder="请选择构建方式"
options={buildTypeOptions}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
name="deployType"
label="部署方式"
rules={[{required: true, message: '请选择部署方式'}]}
>
<Select
placeholder="请选择部署方式"
options={deployTypeOptions}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
name="envDesc"
label="环境描述"
rules={[{max: 200, message: '环境描述最多200个字符'}]}
>
<Input.TextArea rows={4} placeholder="请输入环境描述" />
<Input.TextArea
placeholder="请输入环境描述"
rows={4}
showCount
maxLength={200}
/>
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
tooltip="数字越小越靠前"
initialValue={0}
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="projectId" hidden>
<Input />
</Form.Item>
<Form.Item name="tenantCode" hidden>
<Input />
<Input type="number" placeholder="请输入排序号"/>
</Form.Item>
</Form>
</Modal>

View File

@ -1,30 +1,57 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Progress } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
CloudServerOutlined,
DatabaseOutlined,
ClusterOutlined,
DesktopOutlined
} from '@ant-design/icons';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, RocketOutlined } from '@ant-design/icons';
import { getEnvironmentList, deleteEnvironment } from './service';
import type { Environment } from './types';
import { BuildTypeEnum, DeployTypeEnum } from './types';
import EnvironmentModal from './components/EnvironmentModal';
const { Title, Text } = Typography;
// 构建方式和部署方式的显示映射
const buildTypeMap = {
[BuildTypeEnum.JENKINS]: {
label: 'Jenkins构建',
color: '#1890ff',
description: '使用Jenkins进行自动化构建和部署'
},
[BuildTypeEnum.GITLAB_RUNNER]: {
label: 'GitLab Runner构建',
color: '#722ed1',
description: '使用GitLab Runner进行CI/CD流程'
},
[BuildTypeEnum.GITHUB_ACTION]: {
label: 'GitHub Action构建',
color: '#52c41a',
description: '使用GitHub Action进行自动化工作流'
},
};
const deployTypeMap = {
[DeployTypeEnum.K8S]: {
label: 'Kubernetes集群部署',
color: '#1890ff',
description: '部署到Kubernetes容器编排平台'
},
[DeployTypeEnum.DOCKER]: {
label: 'Docker容器部署',
color: '#13c2c2',
description: '部署为Docker独立容器'
},
[DeployTypeEnum.VM]: {
label: '虚拟机部署',
color: '#faad14',
description: '部署到传统虚拟机环境'
},
};
const EnvironmentList: React.FC = () => {
const [environments, setEnvironments] = useState<Environment[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentEnvironment, setCurrentEnvironment] = useState<Environment>();
// TODO: 这里需要从上下文或者URL参数获取projectId
const projectId = 1;
const fetchEnvironments = async () => {
setLoading(true);
try {
@ -37,7 +64,7 @@ const EnvironmentList: React.FC = () => {
}
};
React.useEffect(() => {
useEffect(() => {
fetchEnvironments();
}, []);
@ -61,49 +88,9 @@ const EnvironmentList: React.FC = () => {
setModalVisible(true);
};
// 根据环境编码获取环境信息
const getEnvInfo = (envCode: string) => {
const envTypes = {
dev: {
icon: <DesktopOutlined />,
name: '开发环境',
color: '#1890ff',
usage: 45
},
test: {
icon: <DatabaseOutlined />,
name: '测试环境',
color: '#52c41a',
usage: 65
},
staging: {
icon: <ClusterOutlined />,
name: '预发环境',
color: '#faad14',
usage: 85
},
prod: {
icon: <CloudServerOutlined />,
name: '生产环境',
color: '#f5222d',
usage: 92
}
};
const envType = Object.entries(envTypes).find(([key]) =>
envCode.toLowerCase().includes(key)
);
return envType ? envType[1] : {
icon: <CloudServerOutlined />,
name: '其他环境',
color: '#8c8c8c',
usage: 50
};
};
const EnvironmentCard = ({ environment }: { environment: Environment }) => {
const envInfo = getEnvInfo(environment.envCode);
const buildType = buildTypeMap[environment.buildType];
const deployType = deployTypeMap[environment.deployType];
return (
<Card
@ -111,27 +98,34 @@ const EnvironmentList: React.FC = () => {
className="environment-card"
style={{
height: '100%',
borderRadius: '12px',
borderRadius: '16px',
overflow: 'hidden',
border: '1px solid #f0f0f0',
border: 'none',
transition: 'all 0.3s',
boxShadow: '0 1px 2px 0 rgba(0,0,0,0.03), 0 1px 6px -1px rgba(0,0,0,0.02), 0 2px 4px 0 rgba(0,0,0,0.02)',
}}
bodyStyle={{
padding: '24px',
background: `linear-gradient(145deg, #ffffff 0%, ${envInfo.color}0A 100%)`,
height: '100%',
display: 'flex',
flexDirection: 'column',
background: `linear-gradient(145deg, #ffffff 0%, ${buildType.color}08 100%)`,
}}
actions={[
<Tooltip title="编辑环境配置">
<Button
type="text"
key="edit"
icon={<EditOutlined style={{ color: envInfo.color }} />}
icon={<EditOutlined style={{ color: buildType.color }} />}
onClick={() => handleEdit(environment)}
>
</Button>,
</Button>
</Tooltip>,
<Popconfirm
key="delete"
title="确定要删除该环境吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(environment.id)}
>
<Button
@ -144,49 +138,31 @@ const EnvironmentList: React.FC = () => {
</Popconfirm>,
]}
>
<div style={{ position: 'relative' }}>
<Space
style={{
position: 'absolute',
top: '-12px',
right: '-12px',
padding: '4px 12px',
background: envInfo.color,
color: '#fff',
borderRadius: '0 0 0 12px',
fontSize: '12px',
alignItems: 'center'
}}
>
{envInfo.icon}
<span>{envInfo.name}</span>
</Space>
<Title level={4} style={{
marginBottom: '16px',
color: '#1f1f1f',
<div style={{ flex: 1 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
alignItems: 'flex-start',
justifyContent: 'space-between',
marginBottom: '16px'
}}>
<Title level={4} style={{
margin: 0,
color: '#1f1f1f',
}}>
{environment.envName}
<Tag
color={environment.envStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
style={{
borderRadius: '4px',
fontSize: '12px',
padding: '0 8px',
}}
>
{environment.envStatus === 'ENABLE' ? '启用' : '禁用'}
</Tag>
</Title>
<RocketOutlined style={{
fontSize: '24px',
color: buildType.color,
opacity: 0.8
}} />
</div>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '20px',
marginBottom: '24px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
@ -202,43 +178,68 @@ const EnvironmentList: React.FC = () => {
display: 'flex',
flexDirection: 'column',
gap: '16px',
padding: '16px',
padding: '20px',
background: '#fff',
borderRadius: '8px',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontFamily: 'monospace',
marginTop: '4px',
fontFamily: 'monaco, monospace',
fontSize: '14px',
padding: '4px 8px',
padding: '8px 12px',
background: '#f5f5f5',
borderRadius: '4px',
borderRadius: '6px',
letterSpacing: '0.5px',
}}>
{environment.envCode}
</div>
</div>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<Tooltip title={buildType.description}>
<Tag
color={buildType.color}
style={{
padding: '4px 12px',
borderRadius: '4px',
cursor: 'help'
}}
>
{buildType.label}
</Tag>
</Tooltip>
</div>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<Tooltip title={deployType.description}>
<Tag
color={deployType.color}
style={{
padding: '4px 12px',
borderRadius: '4px',
cursor: 'help'
}}
>
{deployType.label}
</Tag>
</Tooltip>
</div>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
color: '#262626',
fontSize: '14px',
padding: '4px 0',
}}>
<Text type="secondary" style={{ fontSize: '12px' }}>使</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>{envInfo.usage}%</Text>
{environment.sort}
</div>
<Progress
percent={envInfo.usage}
strokeColor={envInfo.color}
size="small"
showInfo={false}
/>
</div>
</Space>
</div>
</div>
</Card>
@ -256,8 +257,10 @@ const EnvironmentList: React.FC = () => {
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '6px',
borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
padding: '0 24px',
}}
>
@ -278,13 +281,14 @@ const EnvironmentList: React.FC = () => {
onClick={handleAdd}
style={{
height: '100%',
borderRadius: '12px',
borderRadius: '16px',
border: '1px dashed #d9d9d9',
background: '#fafafa',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '250px',
minHeight: '380px',
transition: 'all 0.3s',
}}
bodyStyle={{
width: '100%',
@ -295,8 +299,8 @@ const EnvironmentList: React.FC = () => {
}}
>
<div style={{ textAlign: 'center' }}>
<PlusOutlined style={{ fontSize: '24px', color: '#8c8c8c' }} />
<div style={{ marginTop: '8px', color: '#8c8c8c' }}></div>
<PlusOutlined style={{ fontSize: '28px', color: '#8c8c8c' }} />
<div style={{ marginTop: '12px', color: '#8c8c8c', fontSize: '16px' }}></div>
</div>
</Card>
</Col>
@ -308,7 +312,6 @@ const EnvironmentList: React.FC = () => {
onCancel={() => setModalVisible(false)}
onSuccess={fetchEnvironments}
initialValues={currentEnvironment}
projectId={projectId}
/>
</PageContainer>
);

View File

@ -1,30 +1,48 @@
import type { BaseQuery } from '@/types/base';
export enum BuildTypeEnum {
JENKINS = 'JENKINS',
GITLAB_RUNNER = 'GITLAB_RUNNER',
GITHUB_ACTION = 'GITHUB_ACTION'
}
export enum DeployTypeEnum {
K8S = 'K8S',
DOCKER = 'DOCKER',
VM = 'VM'
}
export interface Environment {
id: number;
tenantCode: string;
projectId: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
buildType: BuildTypeEnum;
deployType: DeployTypeEnum;
}
export interface CreateEnvironmentRequest {
tenantCode: string;
projectId: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
buildType: BuildTypeEnum;
deployType: DeployTypeEnum;
}
export interface UpdateEnvironmentRequest extends CreateEnvironmentRequest {
export interface UpdateEnvironmentRequest {
id: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
buildType: BuildTypeEnum;
deployType: DeployTypeEnum;
}
export interface EnvironmentQuery extends BaseQuery {
tenantCode?: string;
projectId?: number;
envCode?: string;
envName?: string;

View File

@ -1,217 +0,0 @@
import React, { useEffect, useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag } from 'antd';
import { PlusOutlined, TeamOutlined, AppstoreOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { getProjectList, deleteProject } from './service';
import type { Project } from './types';
import ProjectModal from './components/ProjectModal';
const { Title, Text } = Typography;
const ProjectList: React.FC = () => {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentProject, setCurrentProject] = useState<Project>();
const fetchProjects = async () => {
setLoading(true);
try {
const data = await getProjectList();
setProjects(data);
} catch (error) {
message.error('获取项目列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleDelete = async (id: number) => {
try {
await deleteProject(id);
message.success('删除成功');
fetchProjects();
} catch (error) {
message.error('删除失败');
}
};
const handleAdd = () => {
setCurrentProject(undefined);
setModalVisible(true);
};
const handleEdit = (project: Project) => {
setCurrentProject(project);
setModalVisible(true);
};
const ProjectCard = ({ project }: { project: Project }) => (
<Card
hoverable
className="project-card"
style={{
height: '100%',
borderRadius: '12px',
overflow: 'hidden',
border: '1px solid #f0f0f0',
transition: 'all 0.3s',
}}
bodyStyle={{
padding: '24px',
background: 'linear-gradient(145deg, #ffffff 0%, #f8f9ff 100%)',
}}
actions={[
<Button
type="text"
key="edit"
icon={<EditOutlined style={{ color: '#1890ff' }} />}
onClick={() => handleEdit(project)}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该项目吗?"
onConfirm={() => handleDelete(project.id)}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>,
]}
>
<div style={{ position: 'relative', paddingTop: '8px' }}>
<Title level={4} style={{ marginBottom: '16px', color: '#1f1f1f' }}>
{project.projectName}
<Tag
color={project.projectStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
style={{
marginLeft: '8px',
borderRadius: '4px',
fontSize: '12px',
padding: '0 8px',
}}
>
{project.projectStatus === 'ENABLE' ? '启用' : '禁用'}
</Tag>
</Title>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '20px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{project.projectDesc || '暂无描述'}
</Text>
<div style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
background: '#f8f9fa',
borderRadius: '8px',
}}>
<div style={{ flex: 1 }}>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
fontFamily: 'monospace',
marginTop: '4px',
fontSize: '14px',
}}>
{project.projectCode}
</div>
</div>
<div style={{ marginLeft: '24px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
textAlign: 'center',
marginTop: '4px',
fontSize: '14px',
}}>
{project.sort}
</div>
</div>
</div>
</div>
</Card>
);
return (
<PageContainer
header={{
title: '项目管理',
extra: [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '6px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
}}
>
</Button>
],
}}
>
<div style={{ padding: '24px' }}>
<Row gutter={[24, 24]}>
{projects.map(project => (
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
<ProjectCard project={project} />
</Col>
))}
<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>
</PageContainer>
);
};
export default ProjectList;

View File

@ -1,41 +0,0 @@
import request from '@/utils/request';
import type { CreateProjectRequest, UpdateProjectRequest, Project, ProjectQueryParams } from './types';
import type { Page } from '@/types/base';
const BASE_URL = '/api/v1/projects';
// 创建项目
export const createProject = (data: CreateProjectRequest) =>
request.post<void>(BASE_URL, data);
// 更新项目
export const updateProject = (data: UpdateProjectRequest) =>
request.put<void>(`${BASE_URL}/${data.id}`, data);
// 删除项目
export const deleteProject = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);
// 获取项目详情
export const getProject = (id: number) =>
request.get<Project>(`${BASE_URL}/${id}`);
// 分页查询项目列表
export const getProjectPage = (params?: ProjectQueryParams) =>
request.get<Page<Project>>(`${BASE_URL}/page`, { params });
// 获取所有项目列表
export const getProjectList = () =>
request.get<Project[]>(BASE_URL);
// 条件查询项目列表
export const getProjectListByCondition = (params?: ProjectQueryParams) =>
request.get<Project[]>(`${BASE_URL}/list`, { params });
// 批量处理项目
export const batchProcessProject = (data: any) =>
request.post<void>(`${BASE_URL}/batch`, data);
// 导出项目数据
export const exportProject = () =>
request.get(`${BASE_URL}/export`, { responseType: 'blob' });

View File

@ -1,33 +0,0 @@
import { BaseResponse, BaseRequest, BaseQuery } from '@/types/base';
// 项目基础信息
export interface Project extends BaseResponse {
tenantId: number;
projectCode: string;
projectName: string;
projectDesc?: string;
projectStatus: string;
sort: number;
}
// 创建项目请求参数
export interface CreateProjectRequest extends BaseRequest {
tenantId: number;
projectCode: string;
projectName: string;
projectDesc?: string;
projectStatus: string;
sort: number;
}
// 更新项目请求参数
export interface UpdateProjectRequest extends CreateProjectRequest {
id: number;
}
// 分页查询参数
export interface ProjectQueryParams extends BaseQuery {
projectName?: string;
projectCode?: string;
projectStatus?: string;
}

View File

@ -1,13 +1,13 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
import type { Project } from '../types';
import { createProject, updateProject } from '../service';
import type { ProjectGroup } from '../types';
import { createProjectGroup, updateProjectGroup } from '../service';
interface ProjectModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: Project;
initialValues?: ProjectGroup;
}
const ProjectModal: React.FC<ProjectModalProps> = ({
@ -29,10 +29,10 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
try {
const values = await form.validateFields();
if (isEdit) {
await updateProject({ ...values, id: initialValues.id });
await updateProjectGroup({ ...values, id: initialValues.id });
message.success('更新成功');
} else {
await createProject(values);
await createProjectGroup(values);
message.success('创建成功');
}
onSuccess();
@ -50,7 +50,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
return (
<Modal
title={isEdit ? '编辑项目' : '新建项目'}
title={isEdit ? '编辑项目' : '新建项目'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
@ -59,38 +59,39 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
<Form
form={form}
layout="vertical"
initialValues={{ projectStatus: 'ENABLE', sort: 0 }}
initialValues={{ projectGroupStatus: 'ENABLE', sort: 0 }}
>
<Form.Item
name="projectCode"
label="项目编码"
name="projectGroupCode"
label="项目编码"
rules={[
{ required: true, message: '请输入项目编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目编码只能包含字母、数字、下划线和连字符' }
{ required: true, message: '请输入项目编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目编码只能包含字母、数字、下划线和连字符' }
]}
>
<Input placeholder="请输入项目编码" />
<Input placeholder="请输入项目编码" disabled={isEdit} />
</Form.Item>
<Form.Item
name="projectName"
label="项目名称"
rules={[{ required: true, message: '请输入项目名称' }]}
name="projectGroupName"
label="项目名称"
rules={[{ required: true, message: '请输入项目名称' }]}
>
<Input placeholder="请输入项目名称" />
<Input placeholder="请输入项目名称" />
</Form.Item>
<Form.Item
name="projectDesc"
label="项目描述"
name="projectGroupDesc"
label="项目描述"
>
<Input.TextArea rows={4} placeholder="请输入项目描述" />
<Input.TextArea rows={4} placeholder="请输入项目描述" />
</Form.Item>
<Form.Item
name="projectStatus"
label="项目状态"
rules={[{ required: true, message: '请选择项目状态' }]}
name="projectGroupStatus"
label="项目组状态"
rules={[{ required: true, message: '请选择项目组状态' }]}
initialValue="ENABLE"
>
<Radio.Group>
<Radio value="ENABLE"></Radio>

View File

@ -0,0 +1,400 @@
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
SearchOutlined,
EnvironmentOutlined,
ClockCircleOutlined,
RocketOutlined
} from '@ant-design/icons';
import { getProjectGroupList, deleteProjectGroup } from './service';
import type { ProjectGroup } from './types';
import ProjectModal from './components/ProjectModal';
const { Title, Text } = Typography;
const { Search } = Input;
const ProjectList: React.FC = () => {
const [projects, setProjects] = useState<ProjectGroup[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentProject, setCurrentProject] = useState<ProjectGroup>();
const [searchText, setSearchText] = useState('');
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) => {
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 => {
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={12}>
<Statistic
title={<Text type="secondary" style={{ fontSize: '13px' }}></Text>}
value={2}
prefix={<EnvironmentOutlined />}
valueStyle={{ fontSize: '16px', color: typeInfo.color }}
/>
</Col>
<Col span={12}>
<Statistic
title={<Text type="secondary" style={{ fontSize: '13px' }}></Text>}
value={5}
prefix={<TeamOutlined />}
valueStyle={{ fontSize: '16px', color: typeInfo.color }}
/>
</Col>
</Row>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<ClockCircleOutlined style={{ color: typeInfo.color }} />
<span>2</span>
</div>
</div>
<div>
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}></Text>
<div style={{
color: '#262626',
fontSize: '14px',
padding: '4px 0',
}}>
{project.sort}
</div>
</div>
<Divider style={{ margin: '8px 0' }} />
<Space size="middle" style={{ justifyContent: 'center' }}>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(project)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
>
</Button>
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(project.id)}
>
<Button
danger
icon={<DeleteOutlined />}
style={{ borderRadius: '6px' }}
>
</Button>
</Popconfirm>
<Button
icon={<TeamOutlined />}
style={{ borderRadius: '6px' }}
>
</Button>
</Space>
</div>
</div>
</Card>
);
};
return (
<PageContainer
header={{
title: '项目组管理',
extra: [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '8px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
height: '40px',
padding: '0 24px',
}}
>
</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: 'DEMO' },
{ label: '平台项目组', value: 'PLATFORM' },
{ label: '业务项目组', value: 'BUSINESS' },
]}
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 project={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>
<ProjectModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={fetchProjects}
initialValues={currentProject}
/>
</PageContainer>
);
};
export default ProjectList;

View File

@ -0,0 +1,41 @@
import request from '@/utils/request';
import type { CreateProjectGroupRequest, UpdateProjectGroupRequest, ProjectGroup, ProjectGroupQueryParams } from './types';
import type { Page } from '@/types/base';
const BASE_URL = '/api/v1/project-group';
// 创建项目
export const createProjectGroup = (data: CreateProjectGroupRequest) =>
request.post<void>(BASE_URL, data);
// 更新项目
export const updateProjectGroup = (data: UpdateProjectGroupRequest) =>
request.put<void>(`${BASE_URL}/${data.id}`, data);
// 删除项目
export const deleteProjectGroup = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);
// 获取项目详情
export const getProjectGroup = (id: number) =>
request.get<ProjectGroup>(`${BASE_URL}/${id}`);
// 分页查询项目列表
export const getProjectGroupPage = (params?: ProjectGroupQueryParams) =>
request.get<Page<ProjectGroup>>(`${BASE_URL}/page`, { params });
// 获取所有项目列表
export const getProjectGroupList = () =>
request.get<ProjectGroup[]>(BASE_URL);
// 条件查询项目列表
export const getProjectGroupListByCondition = (params?: ProjectGroupQueryParams) =>
request.get<ProjectGroup[]>(`${BASE_URL}/list`, { params });
// 批量处理项目
export const batchProcessProjectGroup = (data: any) =>
request.post<void>(`${BASE_URL}/batch`, data);
// 导出项目数据
export const exportProjectGroup = () =>
request.get(`${BASE_URL}/export`, { responseType: 'blob' });

View File

@ -0,0 +1,33 @@
import { BaseResponse, BaseRequest, BaseQuery } from '@/types/base';
// 项目基础信息
export interface ProjectGroup extends BaseResponse {
tenantCode: string;
projectGroupCode: string;
projectGroupName: string;
projectGroupDesc?: string;
projectGroupStatus: string;
sort: number;
}
// 创建项目请求参数
export interface CreateProjectGroupRequest extends BaseRequest {
tenantCode: string;
projectGroupCode: string;
projectGroupName: string;
projectGroupDesc?: string;
projectGroupStatus: string;
sort: number;
}
// 更新项目请求参数
export interface UpdateProjectGroupRequest extends CreateProjectGroupRequest {
id: number;
}
// 分页查询参数
export interface ProjectGroupQueryParams extends BaseQuery {
projectGroupName?: string;
projectGroupCode?: string;
projectGroupStatus?: string;
}

View File

@ -39,7 +39,7 @@ const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
const LogStreamPage = lazy(() => import('../pages/LogStream'));
const NodeDesign = lazy(() => import('../pages/Workflow/NodeDesign'));
const NodeDesignForm = lazy(() => import('../pages/Workflow/NodeDesign/Design'));
const ProjectList = lazy(() => import('../pages/Deploy/Project/List'));
const ProjectGroupList = lazy(() => import('../pages/Deploy/ProjectGroup/List'));
const ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
@ -73,15 +73,15 @@ const router = createBrowserRouter([
path: 'deploy',
children: [
{
path: 'project',
element: <Suspense fallback={<LoadingComponent/>}><ProjectList/></Suspense>
path: 'project-group',
element: <Suspense fallback={<LoadingComponent/>}><ProjectGroupList/></Suspense>
},
{
path: 'application',
path: 'applications',
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
},
{
path: 'environment',
path: 'environments',
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
}
]