1
This commit is contained in:
parent
cf0b5589d4
commit
434d21d501
@ -8,7 +8,7 @@ interface ApplicationModalProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
initialValues?: Application;
|
initialValues?: Application;
|
||||||
projectId: number;
|
projectGroupId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||||
@ -16,7 +16,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
initialValues,
|
initialValues,
|
||||||
projectId,
|
projectGroupId,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const isEdit = !!initialValues;
|
const isEdit = !!initialValues;
|
||||||
@ -26,19 +26,28 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
form.setFieldsValue(initialValues);
|
form.setFieldsValue(initialValues);
|
||||||
} else {
|
} 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 () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
projectGroupId: projectGroupId // 确保提交时包含项目组ID
|
||||||
|
};
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await updateApplication({ ...values, id: initialValues.id });
|
await updateApplication({ ...submitData, id: initialValues?.id });
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
} else {
|
} else {
|
||||||
await createApplication(values);
|
await createApplication(submitData);
|
||||||
message.success('创建成功');
|
message.success('创建成功');
|
||||||
}
|
}
|
||||||
onSuccess();
|
onSuccess();
|
||||||
@ -67,6 +76,11 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{ appStatus: 'ENABLE', sort: 0 }}
|
initialValues={{ appStatus: 'ENABLE', sort: 0 }}
|
||||||
>
|
>
|
||||||
|
{/* 添加一个隐藏的表单项来存储项目组ID */}
|
||||||
|
<Form.Item name="projectGroupId" hidden>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="appCode"
|
name="appCode"
|
||||||
label="应用编码"
|
label="应用编码"
|
||||||
@ -75,7 +89,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
{ pattern: /^[A-Za-z0-9_-]+$/, message: '应用编码只能包含字母、数字、下划线和连字符' }
|
{ pattern: /^[A-Za-z0-9_-]+$/, message: '应用编码只能包含字母、数字、下划线和连字符' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入应用编码" />
|
<Input placeholder="请输入应用编码" disabled={isEdit} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -111,10 +125,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
|
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="projectId" hidden>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,27 +1,46 @@
|
|||||||
import React, { useState } 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 } 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 { PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons';
|
||||||
import { getApplicationList, deleteApplication } from './service';
|
import { getApplicationList, deleteApplication } from './service';
|
||||||
|
import { getProjectGroupList } from '../../ProjectGroup/List/service';
|
||||||
import type { Application } from './types';
|
import type { Application } from './types';
|
||||||
|
import type { ProjectGroup } from '../../ProjectGroup/List/types';
|
||||||
import ApplicationModal from './components/ApplicationModal';
|
import ApplicationModal from './components/ApplicationModal';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
const ApplicationList: React.FC = () => {
|
const ApplicationList: React.FC = () => {
|
||||||
const [applications, setApplications] = useState<Application[]>([]);
|
const [applications, setApplications] = useState<Application[]>([]);
|
||||||
|
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
|
||||||
|
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
|
||||||
const [loading, setLoading] = useState(false);
|
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>();
|
||||||
|
|
||||||
// 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 () => {
|
const fetchApplications = async () => {
|
||||||
|
if (!selectedProjectGroupId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getApplicationList();
|
const data = await getApplicationList();
|
||||||
setApplications(data);
|
// TODO: 这里需要根据selectedProjectId筛选应用
|
||||||
|
setApplications(data.filter(app => app.projectId === selectedProjectGroupId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取应用列表失败');
|
message.error('获取应用列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
@ -29,10 +48,14 @@ const ApplicationList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchProjects();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApplications();
|
||||||
|
}, [selectedProjectGroupId]);
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await deleteApplication(id);
|
await deleteApplication(id);
|
||||||
@ -53,6 +76,10 @@ const ApplicationList: React.FC = () => {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProjectChange = (value: number) => {
|
||||||
|
setSelectedProjectGroupId(value);
|
||||||
|
};
|
||||||
|
|
||||||
// 根据应用编码推测应用类型
|
// 根据应用编码推测应用类型
|
||||||
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' };
|
||||||
@ -201,10 +228,31 @@ const ApplicationList: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentProject = projectGroups.find(p => p.id === selectedProjectGroupId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
header={{
|
header={{
|
||||||
title: '应用管理',
|
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: [
|
extra: [
|
||||||
<Button
|
<Button
|
||||||
key="add"
|
key="add"
|
||||||
@ -215,6 +263,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
|
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
|
||||||
}}
|
}}
|
||||||
|
disabled={!selectedProjectGroupId}
|
||||||
>
|
>
|
||||||
新建应用
|
新建应用
|
||||||
</Button>
|
</Button>
|
||||||
@ -228,6 +277,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
<ApplicationCard application={application} />
|
<ApplicationCard application={application} />
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
|
{selectedProjectGroupId && (
|
||||||
<Col xs={24} sm={12} md={8} lg={6}>
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
@ -256,6 +306,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -264,7 +315,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
onCancel={() => setModalVisible(false)}
|
onCancel={() => setModalVisible(false)}
|
||||||
onSuccess={fetchApplications}
|
onSuccess={fetchApplications}
|
||||||
initialValues={currentApplication}
|
initialValues={currentApplication}
|
||||||
projectId={projectId}
|
projectGroupId={selectedProjectGroupId!}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import { Modal, Form, Input, InputNumber, message } from 'antd';
|
import {Modal, Form, Input, Select, message} from 'antd';
|
||||||
import type {Environment} from '../types';
|
import type {Environment} from '../types';
|
||||||
|
import {BuildTypeEnum, DeployTypeEnum} from '../types';
|
||||||
import {createEnvironment, updateEnvironment} from '../service';
|
import {createEnvironment, updateEnvironment} from '../service';
|
||||||
|
|
||||||
interface EnvironmentModalProps {
|
interface EnvironmentModalProps {
|
||||||
@ -8,36 +9,47 @@ interface EnvironmentModalProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
initialValues?: Environment;
|
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> = ({
|
const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
initialValues,
|
initialValues,
|
||||||
projectId,
|
|
||||||
tenantCode,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const isEdit = !!initialValues;
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (initialValues) {
|
form.setFieldsValue(initialValues || {});
|
||||||
form.setFieldsValue(initialValues);
|
|
||||||
} else {
|
|
||||||
form.setFieldsValue({ projectId, tenantCode, sort: 0 });
|
|
||||||
}
|
}
|
||||||
}
|
}, [visible, initialValues]);
|
||||||
}, [visible, initialValues, form, projectId, tenantCode]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleOk = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
if (isEdit) {
|
setLoading(true);
|
||||||
await updateEnvironment({ ...values, id: initialValues.id });
|
if (initialValues?.id) {
|
||||||
|
await updateEnvironment({
|
||||||
|
...values,
|
||||||
|
id: initialValues.id,
|
||||||
|
});
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
} else {
|
} else {
|
||||||
await createEnvironment(values);
|
await createEnvironment(values);
|
||||||
@ -45,70 +57,98 @@ const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
|
|||||||
}
|
}
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onCancel();
|
onCancel();
|
||||||
form.resetFields();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(isEdit ? '更新失败' : '创建失败');
|
console.error('Submit failed:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
form.resetFields();
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={isEdit ? '编辑环境' : '新建环境'}
|
title={initialValues ? '编辑环境' : '新建环境'}
|
||||||
open={visible}
|
open={visible}
|
||||||
onOk={handleSubmit}
|
onOk={handleOk}
|
||||||
onCancel={handleCancel}
|
onCancel={onCancel}
|
||||||
|
confirmLoading={loading}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
width={600}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
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
|
<Form.Item
|
||||||
name="envCode"
|
name="envCode"
|
||||||
label="环境编码"
|
label="环境编码"
|
||||||
rules={[
|
rules={[
|
||||||
{required: true, message: '请输入环境编码'},
|
{required: true, message: '请输入环境编码'},
|
||||||
{ pattern: /^[A-Za-z0-9_-]+$/, message: '环境编码只能包含字母、数字、下划线和连字符' }
|
{max: 50, message: '环境编码最多50个字符'},
|
||||||
|
{
|
||||||
|
pattern: /^[A-Za-z0-9_-]+$/,
|
||||||
|
message: '环境编码只能包含字母、数字、下划线和连字符'
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入环境编码"/>
|
<Input placeholder="请输入环境编码"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="envName"
|
name="buildType"
|
||||||
label="环境名称"
|
label="构建方式"
|
||||||
rules={[{ required: true, message: '请输入环境名称' }]}
|
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>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="envDesc"
|
name="envDesc"
|
||||||
label="环境描述"
|
label="环境描述"
|
||||||
|
rules={[{max: 200, message: '环境描述最多200个字符'}]}
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={4} placeholder="请输入环境描述" />
|
<Input.TextArea
|
||||||
|
placeholder="请输入环境描述"
|
||||||
|
rows={4}
|
||||||
|
showCount
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="sort"
|
name="sort"
|
||||||
label="排序"
|
label="排序"
|
||||||
rules={[{ required: true, message: '请输入排序值' }]}
|
tooltip="数字越小越靠前"
|
||||||
|
initialValue={0}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
|
<Input type="number" placeholder="请输入排序号"/>
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item name="projectId" hidden>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item name="tenantCode" hidden>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -1,30 +1,57 @@
|
|||||||
import React, { useState } 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, Progress } from 'antd';
|
import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Tooltip } from 'antd';
|
||||||
import {
|
import { PlusOutlined, EditOutlined, DeleteOutlined, RocketOutlined } from '@ant-design/icons';
|
||||||
PlusOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
CloudServerOutlined,
|
|
||||||
DatabaseOutlined,
|
|
||||||
ClusterOutlined,
|
|
||||||
DesktopOutlined
|
|
||||||
} 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 EnvironmentModal from './components/EnvironmentModal';
|
import EnvironmentModal from './components/EnvironmentModal';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
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 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>();
|
||||||
|
|
||||||
// TODO: 这里需要从上下文或者URL参数获取projectId
|
|
||||||
const projectId = 1;
|
|
||||||
|
|
||||||
const fetchEnvironments = async () => {
|
const fetchEnvironments = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -37,7 +64,7 @@ const EnvironmentList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEnvironments();
|
fetchEnvironments();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -61,49 +88,9 @@ const EnvironmentList: React.FC = () => {
|
|||||||
setModalVisible(true);
|
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 EnvironmentCard = ({ environment }: { environment: Environment }) => {
|
||||||
const envInfo = getEnvInfo(environment.envCode);
|
const buildType = buildTypeMap[environment.buildType];
|
||||||
|
const deployType = deployTypeMap[environment.deployType];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -111,27 +98,34 @@ const EnvironmentList: React.FC = () => {
|
|||||||
className="environment-card"
|
className="environment-card"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '12px',
|
borderRadius: '16px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: '1px solid #f0f0f0',
|
border: 'none',
|
||||||
transition: 'all 0.3s',
|
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={{
|
bodyStyle={{
|
||||||
padding: '24px',
|
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={[
|
actions={[
|
||||||
|
<Tooltip title="编辑环境配置">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
key="edit"
|
key="edit"
|
||||||
icon={<EditOutlined style={{ color: envInfo.color }} />}
|
icon={<EditOutlined style={{ color: buildType.color }} />}
|
||||||
onClick={() => handleEdit(environment)}
|
onClick={() => handleEdit(environment)}
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>,
|
</Button>
|
||||||
|
</Tooltip>,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="delete"
|
key="delete"
|
||||||
title="确定要删除该环境吗?"
|
title="确定要删除该环境吗?"
|
||||||
|
description="删除后将无法恢复,请谨慎操作"
|
||||||
onConfirm={() => handleDelete(environment.id)}
|
onConfirm={() => handleDelete(environment.id)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@ -144,49 +138,31 @@ const EnvironmentList: React.FC = () => {
|
|||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Space
|
<div style={{
|
||||||
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',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
gap: '8px'
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
<Title level={4} style={{
|
||||||
|
margin: 0,
|
||||||
|
color: '#1f1f1f',
|
||||||
}}>
|
}}>
|
||||||
{environment.envName}
|
{environment.envName}
|
||||||
<Tag
|
|
||||||
color={environment.envStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
|
|
||||||
style={{
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px',
|
|
||||||
padding: '0 8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{environment.envStatus === 'ENABLE' ? '启用' : '禁用'}
|
|
||||||
</Tag>
|
|
||||||
</Title>
|
</Title>
|
||||||
|
<RocketOutlined style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: buildType.color,
|
||||||
|
opacity: 0.8
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
type="secondary"
|
type="secondary"
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: '20px',
|
marginBottom: '24px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
@ -202,43 +178,68 @@ const EnvironmentList: React.FC = () => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '20px',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '12px',
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>环境编码</Text>
|
<Text type="secondary" style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}>环境编码</Text>
|
||||||
<div style={{
|
<div style={{
|
||||||
color: '#262626',
|
color: '#262626',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monaco, monospace',
|
||||||
marginTop: '4px',
|
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
padding: '4px 8px',
|
padding: '8px 12px',
|
||||||
background: '#f5f5f5',
|
background: '#f5f5f5',
|
||||||
borderRadius: '4px',
|
borderRadius: '6px',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
}}>
|
}}>
|
||||||
{environment.envCode}
|
{environment.envCode}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<div>
|
<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={{
|
<div style={{
|
||||||
display: 'flex',
|
color: '#262626',
|
||||||
justifyContent: 'space-between',
|
fontSize: '14px',
|
||||||
alignItems: 'center',
|
padding: '4px 0',
|
||||||
marginBottom: '8px'
|
|
||||||
}}>
|
}}>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>资源使用率</Text>
|
{environment.sort}
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>{envInfo.usage}%</Text>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
|
||||||
percent={envInfo.usage}
|
|
||||||
strokeColor={envInfo.color}
|
|
||||||
size="small"
|
|
||||||
showInfo={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@ -256,8 +257,10 @@ const EnvironmentList: React.FC = () => {
|
|||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '6px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
|
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}
|
onClick={handleAdd}
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '12px',
|
borderRadius: '16px',
|
||||||
border: '1px dashed #d9d9d9',
|
border: '1px dashed #d9d9d9',
|
||||||
background: '#fafafa',
|
background: '#fafafa',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
minHeight: '250px',
|
minHeight: '380px',
|
||||||
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -295,8 +299,8 @@ const EnvironmentList: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<PlusOutlined style={{ fontSize: '24px', color: '#8c8c8c' }} />
|
<PlusOutlined style={{ fontSize: '28px', color: '#8c8c8c' }} />
|
||||||
<div style={{ marginTop: '8px', color: '#8c8c8c' }}>新建环境</div>
|
<div style={{ marginTop: '12px', color: '#8c8c8c', fontSize: '16px' }}>新建环境</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@ -308,7 +312,6 @@ const EnvironmentList: React.FC = () => {
|
|||||||
onCancel={() => setModalVisible(false)}
|
onCancel={() => setModalVisible(false)}
|
||||||
onSuccess={fetchEnvironments}
|
onSuccess={fetchEnvironments}
|
||||||
initialValues={currentEnvironment}
|
initialValues={currentEnvironment}
|
||||||
projectId={projectId}
|
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,30 +1,48 @@
|
|||||||
import type { BaseQuery } from '@/types/base';
|
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 {
|
export interface Environment {
|
||||||
id: number;
|
id: number;
|
||||||
tenantCode: string;
|
|
||||||
projectId: number;
|
|
||||||
envCode: string;
|
envCode: string;
|
||||||
envName: string;
|
envName: string;
|
||||||
envDesc?: string;
|
envDesc?: string;
|
||||||
sort: number;
|
sort: number;
|
||||||
|
buildType: BuildTypeEnum;
|
||||||
|
deployType: DeployTypeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateEnvironmentRequest {
|
export interface CreateEnvironmentRequest {
|
||||||
tenantCode: string;
|
|
||||||
projectId: number;
|
projectId: number;
|
||||||
envCode: string;
|
envCode: string;
|
||||||
envName: string;
|
envName: string;
|
||||||
envDesc?: string;
|
envDesc?: string;
|
||||||
sort: number;
|
sort: number;
|
||||||
|
buildType: BuildTypeEnum;
|
||||||
|
deployType: DeployTypeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEnvironmentRequest extends CreateEnvironmentRequest {
|
export interface UpdateEnvironmentRequest {
|
||||||
id: number;
|
id: number;
|
||||||
|
envCode: string;
|
||||||
|
envName: string;
|
||||||
|
envDesc?: string;
|
||||||
|
sort: number;
|
||||||
|
buildType: BuildTypeEnum;
|
||||||
|
deployType: DeployTypeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnvironmentQuery extends BaseQuery {
|
export interface EnvironmentQuery extends BaseQuery {
|
||||||
tenantCode?: string;
|
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
envCode?: string;
|
envCode?: string;
|
||||||
envName?: string;
|
envName?: string;
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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' });
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
|
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
|
||||||
import type { Project } from '../types';
|
import type { ProjectGroup } from '../types';
|
||||||
import { createProject, updateProject } from '../service';
|
import { createProjectGroup, updateProjectGroup } from '../service';
|
||||||
|
|
||||||
interface ProjectModalProps {
|
interface ProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
initialValues?: Project;
|
initialValues?: ProjectGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectModal: React.FC<ProjectModalProps> = ({
|
const ProjectModal: React.FC<ProjectModalProps> = ({
|
||||||
@ -29,10 +29,10 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
|
|||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await updateProject({ ...values, id: initialValues.id });
|
await updateProjectGroup({ ...values, id: initialValues.id });
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
} else {
|
} else {
|
||||||
await createProject(values);
|
await createProjectGroup(values);
|
||||||
message.success('创建成功');
|
message.success('创建成功');
|
||||||
}
|
}
|
||||||
onSuccess();
|
onSuccess();
|
||||||
@ -50,7 +50,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={isEdit ? '编辑项目' : '新建项目'}
|
title={isEdit ? '编辑项目组' : '新建项目组'}
|
||||||
open={visible}
|
open={visible}
|
||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
@ -59,38 +59,39 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
|
|||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{ projectStatus: 'ENABLE', sort: 0 }}
|
initialValues={{ projectGroupStatus: 'ENABLE', sort: 0 }}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="projectCode"
|
name="projectGroupCode"
|
||||||
label="项目编码"
|
label="项目组编码"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入项目编码' },
|
{ required: true, message: '请输入项目组编码' },
|
||||||
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目编码只能包含字母、数字、下划线和连字符' }
|
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目组编码只能包含字母、数字、下划线和连字符' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入项目编码" />
|
<Input placeholder="请输入项目组编码" disabled={isEdit} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="projectName"
|
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="projectDesc"
|
name="projectGroupDesc"
|
||||||
label="项目描述"
|
label="项目组描述"
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={4} placeholder="请输入项目描述" />
|
<Input.TextArea rows={4} placeholder="请输入项目组描述" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="projectStatus"
|
name="projectGroupStatus"
|
||||||
label="项目状态"
|
label="项目组状态"
|
||||||
rules={[{ required: true, message: '请选择项目状态' }]}
|
rules={[{ required: true, message: '请选择项目组状态' }]}
|
||||||
|
initialValue="ENABLE"
|
||||||
>
|
>
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
<Radio value="ENABLE">启用</Radio>
|
<Radio value="ENABLE">启用</Radio>
|
||||||
400
frontend/src/pages/Deploy/ProjectGroup/List/index.tsx
Normal file
400
frontend/src/pages/Deploy/ProjectGroup/List/index.tsx
Normal 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;
|
||||||
41
frontend/src/pages/Deploy/ProjectGroup/List/service.ts
Normal file
41
frontend/src/pages/Deploy/ProjectGroup/List/service.ts
Normal 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' });
|
||||||
33
frontend/src/pages/Deploy/ProjectGroup/List/types.ts
Normal file
33
frontend/src/pages/Deploy/ProjectGroup/List/types.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
|||||||
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||||
const NodeDesign = lazy(() => import('../pages/Workflow/NodeDesign'));
|
const NodeDesign = lazy(() => import('../pages/Workflow/NodeDesign'));
|
||||||
const NodeDesignForm = lazy(() => import('../pages/Workflow/NodeDesign/Design'));
|
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 ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
|
||||||
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
|
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
|
||||||
|
|
||||||
@ -73,15 +73,15 @@ const router = createBrowserRouter([
|
|||||||
path: 'deploy',
|
path: 'deploy',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'project',
|
path: 'project-group',
|
||||||
element: <Suspense fallback={<LoadingComponent/>}><ProjectList/></Suspense>
|
element: <Suspense fallback={<LoadingComponent/>}><ProjectGroupList/></Suspense>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'application',
|
path: 'applications',
|
||||||
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'environment',
|
path: 'environments',
|
||||||
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
|
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user