This commit is contained in:
dengqichen 2024-12-23 15:12:36 +08:00
parent 66a59ad039
commit 48f8459331
15 changed files with 1532 additions and 150 deletions

View File

@ -0,0 +1,123 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
import type { Application } from '../types';
import { createApplication, updateApplication } from '../service';
interface ApplicationModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: Application;
projectId: number;
}
const ApplicationModal: React.FC<ApplicationModalProps> = ({
visible,
onCancel,
onSuccess,
initialValues,
projectId,
}) => {
const [form] = Form.useForm();
const isEdit = !!initialValues;
useEffect(() => {
if (visible) {
if (initialValues) {
form.setFieldsValue(initialValues);
} else {
form.setFieldsValue({ projectId, appStatus: 'ENABLE', sort: 0 });
}
}
}, [visible, initialValues, form, projectId]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (isEdit) {
await updateApplication({ ...values, id: initialValues.id });
message.success('更新成功');
} else {
await createApplication(values);
message.success('创建成功');
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={isEdit ? '编辑应用' : '新建应用'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{ appStatus: 'ENABLE', sort: 0 }}
>
<Form.Item
name="appCode"
label="应用编码"
rules={[
{ required: true, message: '请输入应用编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '应用编码只能包含字母、数字、下划线和连字符' }
]}
>
<Input placeholder="请输入应用编码" />
</Form.Item>
<Form.Item
name="appName"
label="应用名称"
rules={[{ required: true, message: '请输入应用名称' }]}
>
<Input placeholder="请输入应用名称" />
</Form.Item>
<Form.Item
name="appDesc"
label="应用描述"
>
<Input.TextArea rows={4} placeholder="请输入应用描述" />
</Form.Item>
<Form.Item
name="appStatus"
label="应用状态"
rules={[{ required: true, message: '请选择应用状态' }]}
>
<Radio.Group>
<Radio value="ENABLE"></Radio>
<Radio value="DISABLE"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="projectId" hidden>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default ApplicationModal;

View File

@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons';
import { getApplicationList, deleteApplication } from './service';
import type { Application } from './types';
import ApplicationModal from './components/ApplicationModal';
const { Title, Text } = Typography;
const ApplicationList: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentApplication, setCurrentApplication] = useState<Application>();
// TODO: 这里需要从上下文或者URL参数获取projectId
const projectId = 1;
const fetchApplications = async () => {
setLoading(true);
try {
const data = await getApplicationList();
setApplications(data);
} catch (error) {
message.error('获取应用列表失败');
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchApplications();
}, []);
const handleDelete = async (id: number) => {
try {
await deleteApplication(id);
message.success('删除成功');
fetchApplications();
} catch (error) {
message.error('删除失败');
}
};
const handleAdd = () => {
setCurrentApplication(undefined);
setModalVisible(true);
};
const handleEdit = (application: Application) => {
setCurrentApplication(application);
setModalVisible(true);
};
// 根据应用编码推测应用类型
const getAppType = (appCode: string) => {
if (appCode.toLowerCase().includes('web')) return { icon: <CodeOutlined />, name: '前端应用', color: '#1890ff' };
if (appCode.toLowerCase().includes('api')) return { icon: <ApiOutlined />, name: 'API服务', color: '#52c41a' };
return { icon: <CloudUploadOutlined />, name: '后端服务', color: '#722ed1' };
};
const ApplicationCard = ({ application }: { application: Application }) => {
const appType = getAppType(application.appCode);
return (
<Card
hoverable
className="application-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%, ${appType.color}0A 100%)`,
}}
actions={[
<Button
type="text"
key="edit"
icon={<EditOutlined style={{ color: appType.color }} />}
onClick={() => handleEdit(application)}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该应用吗?"
onConfirm={() => handleDelete(application.id)}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>,
]}
>
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '-12px',
right: '-12px',
padding: '4px 8px',
background: appType.color,
color: '#fff',
borderRadius: '0 0 0 8px',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
{appType.icon}
{appType.name}
</div>
<Title level={4} style={{
marginBottom: '16px',
color: '#1f1f1f',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{application.appName}
<Tag
color={application.appStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
style={{
borderRadius: '4px',
fontSize: '12px',
padding: '0 8px',
}}
>
{application.appStatus === 'ENABLE' ? '启用' : '禁用'}
</Tag>
</Title>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '20px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{application.appDesc || '暂无描述'}
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
padding: '16px',
background: '#fff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
fontFamily: 'monospace',
marginTop: '4px',
fontSize: '14px',
padding: '4px 8px',
background: '#f5f5f5',
borderRadius: '4px',
}}>
{application.appCode}
</div>
</div>
<Space size="large">
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
textAlign: 'center',
marginTop: '4px',
fontSize: '14px',
}}>
{application.sort}
</div>
</div>
<Tooltip title="更多功能开发中...">
<Button type="text" size="small"></Button>
</Tooltip>
</Space>
</div>
</div>
</Card>
);
};
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]}>
{applications.map(application => (
<Col xs={24} sm={12} md={8} lg={6} key={application.id}>
<ApplicationCard application={application} />
</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>
<ApplicationModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={fetchApplications}
initialValues={currentApplication}
projectId={projectId}
/>
</PageContainer>
);
};
export default ApplicationList;

View File

@ -0,0 +1,33 @@
import request from '@/utils/request';
import type { CreateApplicationRequest, UpdateApplicationRequest, Application, ApplicationQuery } from './types';
import type { Page } from '@/types/base';
const BASE_URL = '/api/v1/applications';
// 创建应用
export const createApplication = (data: CreateApplicationRequest) =>
request.post<void>(BASE_URL, data);
// 更新应用
export const updateApplication = (data: UpdateApplicationRequest) =>
request.put<void>(`${BASE_URL}/${data.id}`, data);
// 删除应用
export const deleteApplication = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);
// 获取应用详情
export const getApplication = (id: number) =>
request.get<Application>(`${BASE_URL}/${id}`);
// 分页查询应用列表
export const getApplicationPage = (params?: ApplicationQuery) =>
request.get<Page<Application>>(`${BASE_URL}/page`, { params });
// 获取所有应用列表
export const getApplicationList = () =>
request.get<Application[]>(BASE_URL);
// 条件查询应用列表
export const getApplicationListByCondition = (params?: ApplicationQuery) =>
request.get<Application[]>(`${BASE_URL}/list`, { params });

View File

@ -0,0 +1,31 @@
import type { BaseQuery } from '@/types/base';
export interface Application {
id: number;
projectId: number;
appCode: string;
appName: string;
appDesc?: string;
appStatus: 'ENABLE' | 'DISABLE';
sort: number;
}
export interface CreateApplicationRequest {
projectId: number;
appCode: string;
appName: string;
appDesc?: string;
appStatus: string;
sort: number;
}
export interface UpdateApplicationRequest extends CreateApplicationRequest {
id: number;
}
export interface ApplicationQuery extends BaseQuery {
projectId?: number;
appCode?: string;
appName?: string;
appStatus?: string;
}

View File

@ -0,0 +1,118 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, message } from 'antd';
import type { Environment } from '../types';
import { createEnvironment, updateEnvironment } from '../service';
interface EnvironmentModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: Environment;
projectId: number;
tenantCode: string;
}
const EnvironmentModal: React.FC<EnvironmentModalProps> = ({
visible,
onCancel,
onSuccess,
initialValues,
projectId,
tenantCode,
}) => {
const [form] = Form.useForm();
const isEdit = !!initialValues;
useEffect(() => {
if (visible) {
if (initialValues) {
form.setFieldsValue(initialValues);
} else {
form.setFieldsValue({ projectId, tenantCode, sort: 0 });
}
}
}, [visible, initialValues, form, projectId, tenantCode]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (isEdit) {
await updateEnvironment({ ...values, id: initialValues.id });
message.success('更新成功');
} else {
await createEnvironment(values);
message.success('创建成功');
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={isEdit ? '编辑环境' : '新建环境'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{ sort: 0 }}
>
<Form.Item
name="envCode"
label="环境编码"
rules={[
{ required: true, message: '请输入环境编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '环境编码只能包含字母、数字、下划线和连字符' }
]}
>
<Input placeholder="请输入环境编码" />
</Form.Item>
<Form.Item
name="envName"
label="环境名称"
rules={[{ required: true, message: '请输入环境名称' }]}
>
<Input placeholder="请输入环境名称" />
</Form.Item>
<Form.Item
name="envDesc"
label="环境描述"
>
<Input.TextArea rows={4} placeholder="请输入环境描述" />
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="projectId" hidden>
<Input />
</Form.Item>
<Form.Item name="tenantCode" hidden>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default EnvironmentModal;

View File

@ -0,0 +1,317 @@
import React, { useState } 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 { getEnvironmentList, deleteEnvironment } from './service';
import type { Environment } from './types';
import EnvironmentModal from './components/EnvironmentModal';
const { Title, Text } = Typography;
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 {
const data = await getEnvironmentList();
setEnvironments(data);
} catch (error) {
message.error('获取环境列表失败');
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchEnvironments();
}, []);
const handleDelete = async (id: number) => {
try {
await deleteEnvironment(id);
message.success('删除成功');
fetchEnvironments();
} catch (error) {
message.error('删除失败');
}
};
const handleAdd = () => {
setCurrentEnvironment(undefined);
setModalVisible(true);
};
const handleEdit = (environment: Environment) => {
setCurrentEnvironment(environment);
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);
return (
<Card
hoverable
className="environment-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%, ${envInfo.color}0A 100%)`,
}}
actions={[
<Button
type="text"
key="edit"
icon={<EditOutlined style={{ color: envInfo.color }} />}
onClick={() => handleEdit(environment)}
>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该环境吗?"
onConfirm={() => handleDelete(environment.id)}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
>
</Button>
</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',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{environment.envName}
<Tag
color={environment.envStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'}
style={{
borderRadius: '4px',
fontSize: '12px',
padding: '0 8px',
}}
>
{environment.envStatus === 'ENABLE' ? '启用' : '禁用'}
</Tag>
</Title>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '20px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{environment.envDesc || '暂无描述'}
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
padding: '16px',
background: '#fff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}></Text>
<div style={{
color: '#262626',
fontFamily: 'monospace',
marginTop: '4px',
fontSize: '14px',
padding: '4px 8px',
background: '#f5f5f5',
borderRadius: '4px',
}}>
{environment.envCode}
</div>
</div>
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
}}>
<Text type="secondary" style={{ fontSize: '12px' }}>使</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>{envInfo.usage}%</Text>
</div>
<Progress
percent={envInfo.usage}
strokeColor={envInfo.color}
size="small"
showInfo={false}
/>
</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]}>
{environments.map(environment => (
<Col xs={24} sm={12} md={8} lg={6} key={environment.id}>
<EnvironmentCard environment={environment} />
</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>
<EnvironmentModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={fetchEnvironments}
initialValues={currentEnvironment}
projectId={projectId}
/>
</PageContainer>
);
};
export default EnvironmentList;

View File

@ -0,0 +1,33 @@
import request from '@/utils/request';
import type { CreateEnvironmentRequest, UpdateEnvironmentRequest, Environment, EnvironmentQuery } from './types';
import type { Page } from '@/types/base';
const BASE_URL = '/api/v1/environments';
// 创建环境
export const createEnvironment = (data: CreateEnvironmentRequest) =>
request.post<void>(BASE_URL, data);
// 更新环境
export const updateEnvironment = (data: UpdateEnvironmentRequest) =>
request.put<void>(`${BASE_URL}/${data.id}`, data);
// 删除环境
export const deleteEnvironment = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);
// 获取环境详情
export const getEnvironment = (id: number) =>
request.get<Environment>(`${BASE_URL}/${id}`);
// 分页查询环境列表
export const getEnvironmentPage = (params?: EnvironmentQuery) =>
request.get<Page<Environment>>(`${BASE_URL}/page`, { params });
// 获取所有环境列表
export const getEnvironmentList = () =>
request.get<Environment[]>(BASE_URL);
// 条件查询环境列表
export const getEnvironmentListByCondition = (params?: EnvironmentQuery) =>
request.get<Environment[]>(`${BASE_URL}/list`, { params });

View File

@ -0,0 +1,31 @@
import type { BaseQuery } from '@/types/base';
export interface Environment {
id: number;
tenantCode: string;
projectId: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
}
export interface CreateEnvironmentRequest {
tenantCode: string;
projectId: number;
envCode: string;
envName: string;
envDesc?: string;
sort: number;
}
export interface UpdateEnvironmentRequest extends CreateEnvironmentRequest {
id: number;
}
export interface EnvironmentQuery extends BaseQuery {
tenantCode?: string;
projectId?: number;
envCode?: string;
envName?: string;
}

View File

@ -0,0 +1,113 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
import type { Project } from '../types';
import { createProject, updateProject } from '../service';
interface ProjectModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: Project;
}
const ProjectModal: React.FC<ProjectModalProps> = ({
visible,
onCancel,
onSuccess,
initialValues,
}) => {
const [form] = Form.useForm();
const isEdit = !!initialValues;
useEffect(() => {
if (visible && initialValues) {
form.setFieldsValue(initialValues);
}
}, [visible, initialValues, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (isEdit) {
await updateProject({ ...values, id: initialValues.id });
message.success('更新成功');
} else {
await createProject(values);
message.success('创建成功');
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={isEdit ? '编辑项目' : '新建项目'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{ projectStatus: 'ENABLE', sort: 0 }}
>
<Form.Item
name="projectCode"
label="项目编码"
rules={[
{ required: true, message: '请输入项目编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目编码只能包含字母、数字、下划线和连字符' }
]}
>
<Input placeholder="请输入项目编码" />
</Form.Item>
<Form.Item
name="projectName"
label="项目名称"
rules={[{ required: true, message: '请输入项目名称' }]}
>
<Input placeholder="请输入项目名称" />
</Form.Item>
<Form.Item
name="projectDesc"
label="项目描述"
>
<Input.TextArea rows={4} placeholder="请输入项目描述" />
</Form.Item>
<Form.Item
name="projectStatus"
label="项目状态"
rules={[{ required: true, message: '请选择项目状态' }]}
>
<Radio.Group>
<Radio value="ENABLE"></Radio>
<Radio value="DISABLE"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
);
};
export default ProjectModal;

View File

@ -0,0 +1,217 @@
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

@ -0,0 +1,41 @@
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

@ -0,0 +1,33 @@
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

@ -6,7 +6,6 @@ import {login} from './service';
import {getEnabledTenants} from '@/pages/System/Tenant/service';
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
import type { MenuResponse } from '@/pages/System/Menu/types';
import type { TenantResponse } from '@/pages/System/Tenant/types';
import styles from './index.module.css';

View File

@ -18,142 +18,142 @@ export const getCurrentUserMenus = async () => {
const menus = await request.get<MenuResponse[]>(`${BASE_URL}/current`);
console.log('Backend menus:', menus);
// 添加首页路由
const dashboard: MenuResponse = {
id: 0,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "首页",
path: "/dashboard",
component: "/Dashboard/index",
icon: "dashboard",
type: MenuTypeEnum.MENU,
parentId: 0,
sort: 0,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system"
};
// 添加工作流菜单
const workflow: MenuResponse = {
id: -2,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "工作流管理",
path: "/workflow",
icon: "apartment",
type: MenuTypeEnum.DIRECTORY,
parentId: 0,
sort: 2,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system",
children: [
{
id: -21,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "流程定义",
path: "/workflow/definition",
component: "/pages/Workflow/Definition/index",
icon: "apartment",
type: MenuTypeEnum.MENU,
parentId: -2,
sort: 0,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system"
},
{
id: 1000,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "流程实例",
path: "/workflow/instance",
component: "/Workflow/Instance/index",
icon: "instance",
type: MenuTypeEnum.DIRECTORY,
parentId: -2,
sort: 1,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system",
children: [
{
id: 1001,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "工作流实例",
path: "/workflow/instance",
component: "/Workflow/Instance/index",
icon: "instance",
type: MenuTypeEnum.MENU,
parentId: 1000,
sort: 1,
hidden: false
},
{
id: 1002,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "节点设计",
path: "/workflow/node-design",
component: "/Workflow/NodeDesign/index",
icon: "node-design",
type: MenuTypeEnum.MENU,
parentId: 1000,
sort: 2,
hidden: false
}
]
},
{
id: -23,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "流程监控",
path: "/workflow/monitor",
component: "/pages/Workflow/Monitor/index",
icon: "dashboard",
type: MenuTypeEnum.MENU,
parentId: -2,
sort: 2,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system"
},
{
id: -24,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "日志流",
path: "/workflow/log-stream/:processInstanceId",
component: "/pages/LogStream/index",
icon: "file-text",
type: MenuTypeEnum.MENU,
parentId: -2,
sort: 3,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system"
}
]
};
// // 添加首页路由
// const dashboard: MenuResponse = {
// id: 0,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "首页",
// path: "/dashboard",
// component: "/Dashboard/index",
// icon: "dashboard",
// type: MenuTypeEnum.MENU,
// parentId: 0,
// sort: 0,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system"
// };
//
// // 添加工作流菜单
// const workflow: MenuResponse = {
// id: -2,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "工作流管理",
// path: "/workflow",
// icon: "apartment",
// type: MenuTypeEnum.DIRECTORY,
// parentId: 0,
// sort: 2,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system",
// children: [
// {
// id: -21,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "流程定义",
// path: "/workflow/definition",
// component: "/pages/Workflow/Definition/index",
// icon: "apartment",
// type: MenuTypeEnum.MENU,
// parentId: -2,
// sort: 0,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system"
// },
// {
// id: 1000,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "流程实例",
// path: "/workflow/instance",
// component: "/Workflow/Instance/index",
// icon: "instance",
// type: MenuTypeEnum.DIRECTORY,
// parentId: -2,
// sort: 1,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system",
// children: [
// {
// id: 1001,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "工作流实例",
// path: "/workflow/instance",
// component: "/Workflow/Instance/index",
// icon: "instance",
// type: MenuTypeEnum.MENU,
// parentId: 1000,
// sort: 1,
// hidden: false
// },
// {
// id: 1002,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "节点设计",
// path: "/workflow/node-design",
// component: "/Workflow/NodeDesign/index",
// icon: "node-design",
// type: MenuTypeEnum.MENU,
// parentId: 1000,
// sort: 2,
// hidden: false
// }
// ]
// },
// {
// id: -23,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "流程监控",
// path: "/workflow/monitor",
// component: "/pages/Workflow/Monitor/index",
// icon: "dashboard",
// type: MenuTypeEnum.MENU,
// parentId: -2,
// sort: 2,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system"
// },
// {
// id: -24,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "日志流",
// path: "/workflow/log-stream/:processInstanceId",
// component: "/pages/LogStream/index",
// icon: "file-text",
// type: MenuTypeEnum.MENU,
// parentId: -2,
// sort: 3,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system"
// }
// ]
// };
// 添加X6测试菜单
// const x6Test: MenuResponse = {
@ -192,7 +192,7 @@ export const getCurrentUserMenus = async () => {
};
const processedMenus = menus.map(processMenu);
const allMenus = [dashboard, workflow, ...processedMenus];
const allMenus = [ ...processedMenus];
console.log('All menus:', allMenus);
return allMenus;
};

View File

@ -39,6 +39,9 @@ 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 ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
// 创建路由
const router = createBrowserRouter([
@ -66,6 +69,23 @@ const router = createBrowserRouter([
</Suspense>
)
},
{
path: 'deploy',
children: [
{
path: 'project',
element: <Suspense fallback={<LoadingComponent/>}><ProjectList/></Suspense>
},
{
path: 'application',
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
},
{
path: 'environment',
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
}
]
},
{
path: 'system',
children: [