feat(componentDeployment): 新增组件部署功能并优化UI交互

- 实现组件上架/下架功能,支持启停线上实例
- 新增环境类型选择器,从API动态获取环境配置
- 优化折叠列表样式,增强状态标签视觉反馈
- 添加搜索与状态筛选功能,提升组件查找效率
- 完善实例管理表格,支持分页加载与状态展示
- 引入模态框确认机制,防止误操作导致服务中断
- 调整页面布局结构,适配不同屏幕尺寸显示效果
master
钟良源 3 months ago
parent ececda16a7
commit 8a6f7183b3

@ -7,3 +7,13 @@ const urlPrefix = '/api/v1/bpms-workbench';
export function getDeployList(params) {
return axios.get(`${urlPrefix}/componentDeploy/list`, { params });
}
// 组件上架
export function componentOnSale(params) {
return axios.get(`${urlPrefix}/componentDeploy/start`, { params });
}
// 组件下架
export function componentOffSale(params) {
return axios.get(`${urlPrefix}/componentDeploy/stop`, { params });
}

@ -0,0 +1,34 @@
import axios from 'axios';
// 公共路径
const urlPrefix = '/api/v1/bpms-workbench';
// 新增实例
export function createInstance(params) {
return axios.post(`${urlPrefix}/componentInstance/create`, { params });
}
// 停止实例
export function stopInstance(params) {
return axios.get(`${urlPrefix}/componentInstance/stop`, { params });
}
// 启动实例
export function startInstance(params) {
return axios.get(`${urlPrefix}/componentInstance/start`, { params });
}
// 刷新实例依赖
export function refreshInstanceDependency(params) {
return axios.get(`${urlPrefix}/componentInstance/refreshDeps`, { params });
}
// 组件实例日志
export function getInstanceLog(params) {
return axios.get(`${urlPrefix}/componentInstance/logs`, { params });
}
// 组件实例列表
export function getInstanceList(params, identifier) {
return axios.get(`${urlPrefix}/componentInstance/list/${identifier}`, { params });
}

@ -1,6 +1,7 @@
import React, { useState } from 'react' ;
import React, { useEffect, useState } from 'react' ;
import { Modal, Form, Select, Grid, Slider, Switch, Input } from '@arco-design/web-react';
import EditableTable from '@/components/EditableTable';
import { getComponentClassify } from '@/api/componentClassify';
const FormItem = Form.Item;
const Option = Select.Option;
@ -76,6 +77,7 @@ const deviceColumns = [
const AddModal = ({ visible, setVisible }) => {
const [currentRunType, setCurrentRunType] = useState('local');
const [envType, setEnvType] = useState([]); // 环境类型
const [tableData, setTableData] = useState([
{
key: '1',
@ -85,6 +87,15 @@ const AddModal = ({ visible, setVisible }) => {
}
]);
const getEnvOptions = async () => {
const res: any = await getComponentClassify('docker-env');
if (res.code === 200) setEnvType(res.data);
};
useEffect(() => {
getEnvOptions();
}, []);
return (
<Modal
title="新增实例"
@ -124,9 +135,9 @@ const AddModal = ({ visible, setVisible }) => {
placeholder="请选择部署环境"
style={{ width: '90%' }}
>
{runTypes.map((option, index) => (
<Option key={option.value} value={option.value}>
{option.label}
{envType.map((option, index) => (
<Option key={option.id} value={option.id}>
{option.classifyName}
</Option>
))}
</Select>
@ -147,136 +158,136 @@ const AddModal = ({ visible, setVisible }) => {
</FormItem>
</Grid.Col>
</Grid.Row>
<Grid.Row gutter={8}>
<Grid.Col span={12}>
<FormItem label="CPU核数" field="a">
<Slider
defaultValue={1}
min={1}
max={4}
step={1}
showTicks={true}
marks={{
1: '1核',
2: '',
3: '',
4: '4核'
}}
style={{
width: '90%',
whiteSpace: 'nowrap'
}}
/>
</FormItem>
</Grid.Col>
<Grid.Col span={12}>
<FormItem label="是否使用GPU" field="b">
<Switch checkedText="ON" uncheckedText="OFF" />
</FormItem>
</Grid.Col>
</Grid.Row>
<FormItem label="最大内存:">
<Slider
defaultValue={2048}
min={64}
max={4069}
step={64}
marks={{
64: '64MB',
1024: '1GB',
2048: '2GB',
4069: '4GB'
}}
showInput={{
hideControl: false,
suffix: 'MB',
style: {
width: 100
}
}}
style={{ width: '100%' }}
/>
</FormItem>
<Grid.Row gutter={8}>
<Grid.Col span={8}>
<FormItem label="网络模式:" field="tags">
<Select
placeholder="请选择网络模式"
style={{ width: '90%' }}
>
{runTypes.map((option, index) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
<Grid.Col span={8}>
<FormItem label="网卡:" field="type">
<Select
placeholder="请选择网卡"
style={{ width: '90%' }}
>
{runTypes.map((option, index) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
<Grid.Col span={8}>
<FormItem label="IP" field="type">
<Input style={{ width: '90%' }} allowClear placeholder="请输入IP" />
</FormItem>
</Grid.Col>
</Grid.Row>
<FormItem label="端口映射:">
<EditableTable
columns={portColumns}
data={tableData}
onDataChange={(newData) => setTableData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
<FormItem label="目录挂载:">
<EditableTable
columns={directoryColumns}
data={tableData}
onDataChange={(newData) => setTableData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
<FormItem label="设备挂载:">
<EditableTable
columns={deviceColumns}
data={tableData}
onDataChange={(newData) => setTableData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
{/*<Grid.Row gutter={8}>*/}
{/* <Grid.Col span={12}>*/}
{/* <FormItem label="CPU核数" field="a">*/}
{/* <Slider*/}
{/* defaultValue={1}*/}
{/* min={1}*/}
{/* max={4}*/}
{/* step={1}*/}
{/* showTicks={true}*/}
{/* marks={{*/}
{/* 1: '1核',*/}
{/* 2: '',*/}
{/* 3: '',*/}
{/* 4: '4核'*/}
{/* }}*/}
{/* style={{*/}
{/* width: '90%',*/}
{/* whiteSpace: 'nowrap'*/}
{/* }}*/}
{/* />*/}
{/* </FormItem>*/}
{/* </Grid.Col>*/}
{/* <Grid.Col span={12}>*/}
{/* <FormItem label="是否使用GPU" field="b">*/}
{/* <Switch checkedText="ON" uncheckedText="OFF" />*/}
{/* </FormItem>*/}
{/* </Grid.Col>*/}
{/*</Grid.Row>*/}
{/*<FormItem label="最大内存:">*/}
{/* <Slider*/}
{/* defaultValue={2048}*/}
{/* min={64}*/}
{/* max={4069}*/}
{/* step={64}*/}
{/* marks={{*/}
{/* 64: '64MB',*/}
{/* 1024: '1GB',*/}
{/* 2048: '2GB',*/}
{/* 4069: '4GB'*/}
{/* }}*/}
{/* showInput={{*/}
{/* hideControl: false,*/}
{/* suffix: 'MB',*/}
{/* style: {*/}
{/* width: 100*/}
{/* }*/}
{/* }}*/}
{/* style={{ width: '100%' }}*/}
{/* />*/}
{/*</FormItem>*/}
{/*<Grid.Row gutter={8}>*/}
{/* <Grid.Col span={8}>*/}
{/* <FormItem label="网络模式:" field="tags">*/}
{/* <Select*/}
{/* placeholder="请选择网络模式"*/}
{/* style={{ width: '90%' }}*/}
{/* >*/}
{/* {runTypes.map((option, index) => (*/}
{/* <Option key={option.value} value={option.value}>*/}
{/* {option.label}*/}
{/* </Option>*/}
{/* ))}*/}
{/* </Select>*/}
{/* </FormItem>*/}
{/* </Grid.Col>*/}
{/* <Grid.Col span={8}>*/}
{/* <FormItem label="网卡:" field="type">*/}
{/* <Select*/}
{/* placeholder="请选择网卡"*/}
{/* style={{ width: '90%' }}*/}
{/* >*/}
{/* {runTypes.map((option, index) => (*/}
{/* <Option key={option.value} value={option.value}>*/}
{/* {option.label}*/}
{/* </Option>*/}
{/* ))}*/}
{/* </Select>*/}
{/* </FormItem>*/}
{/* </Grid.Col>*/}
{/* <Grid.Col span={8}>*/}
{/* <FormItem label="IP" field="type">*/}
{/* <Input style={{ width: '90%' }} allowClear placeholder="请输入IP" />*/}
{/* </FormItem>*/}
{/* </Grid.Col>*/}
{/*</Grid.Row>*/}
{/*<FormItem label="端口映射:">*/}
{/* <EditableTable*/}
{/* columns={portColumns}*/}
{/* data={tableData}*/}
{/* onDataChange={(newData) => setTableData(newData)}*/}
{/* showAddButton={true}*/}
{/* addButtonText="添加行"*/}
{/* showDeleteButton={true}*/}
{/* deleteButtonText="删除"*/}
{/* tableProps={{*/}
{/* pagination: { pageSize: 5 },*/}
{/* scroll: { y: 400 }*/}
{/* }}*/}
{/* />*/}
{/*</FormItem>*/}
{/*<FormItem label="目录挂载:">*/}
{/* <EditableTable*/}
{/* columns={directoryColumns}*/}
{/* data={tableData}*/}
{/* onDataChange={(newData) => setTableData(newData)}*/}
{/* showAddButton={true}*/}
{/* addButtonText="添加行"*/}
{/* showDeleteButton={true}*/}
{/* deleteButtonText="删除"*/}
{/* tableProps={{*/}
{/* pagination: { pageSize: 5 },*/}
{/* scroll: { y: 400 }*/}
{/* }}*/}
{/* />*/}
{/*</FormItem>*/}
{/*<FormItem label="设备挂载:">*/}
{/* <EditableTable*/}
{/* columns={deviceColumns}*/}
{/* data={tableData}*/}
{/* onDataChange={(newData) => setTableData(newData)}*/}
{/* showAddButton={true}*/}
{/* addButtonText="添加行"*/}
{/* showDeleteButton={true}*/}
{/* deleteButtonText="删除"*/}
{/* tableProps={{*/}
{/* pagination: { pageSize: 5 },*/}
{/* scroll: { y: 400 }*/}
{/* }}*/}
{/* />*/}
{/*</FormItem>*/}
</>
)
}

@ -1,29 +1,77 @@
import React, { useEffect, useState } from 'react';
import { Collapse, Space, Tag, Button, Table, TableColumnProps } from '@arco-design/web-react';
import { Collapse, Space, Tag, Button, Table, TableColumnProps, Modal } from '@arco-design/web-react';
import styles from './style/collapseList.module.less';
import ListNode from '@/pages/componentDevelopment/componentDeployment/listNode';
import AddModal from '@/pages/componentDevelopment/componentDeployment/addModal';
import { getDeployList } from '@/api/componentDeploy';
import { componentOnSale, componentOffSale, getDeployList } from '@/api/componentDeploy';
import { runStatusConstant, runStatusDic } from '@/const/isdp/componentDeploy';
const CollapseItem = Collapse.Item;
const CollapseList = () => {
interface CollapseListProps {
searchKeyword?: string;
runStatus?: string;
}
const CollapseList: React.FC<CollapseListProps> = ({ searchKeyword, runStatus }) => {
const [collapses, setCollapses] = useState([]);
const [visible, setVisible] = useState(false);
const [showOffSaleModal, setShowOffSaleModal] = useState(false);
const [selectedItem, setSelectedItem] = useState(null); // 选中的折叠面板数据
const [expandedItem, setExpandedItem] = useState(null); // 当前展开的折叠面板数据
// 获取组件列表
const getList = async () => {
const params = {
const params: any = {
current: 1,
size: 10
};
// 添加搜索关键词
if (searchKeyword) {
params.name = searchKeyword;
}
// 添加运行状态筛选
if (runStatus) {
params.runStatus = runStatus;
}
const res: any = await getDeployList(params);
if (res.code === 200) setCollapses(res.data.records);
};
// 上架组件
const componentOnSaleHandler = async (value) => {
const res: any = await componentOnSale({ identifier: value.identifier });
if (res.code === 200) getList();
else {
Modal.error({
title: '上架失败',
content: res.message
});
}
};
// 下架组件
const componentOffSaleHandler = async (stopInstance: boolean) => {
const res: any = await componentOffSale({ identifier: selectedItem.identifier, stopInstance });
if (res.code === 200) {
getList();
setShowOffSaleModal(false);
}
else {
Modal.error({
title: '下架失败',
content: res.message
});
}
};
useEffect(() => {
getList();
}, []);
}, [searchKeyword, runStatus]);
const headerNode = (item) => {
const getRunStatus = () => {
@ -54,26 +102,42 @@ const CollapseList = () => {
);
};
const extraNode = () => {
const extraNode = (item) => {
return (
<div className={styles['extra-node']}>
<Space size={20}>
{/*<div className={styles['flex-box']}>*/}
{/* <img src={'/icons/compileIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />*/}
{/* <span style={{ color: '#A2A2AB' }}>未编译</span>*/}
{/*</div>*/}
{/*<div className={styles['flex-box']}>*/}
{/* <img src={'/icons/recompileIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />*/}
{/* <span style={{ color: '#A2A2AB' }}>重新编译</span>*/}
{/*</div>*/}
{/*新增实例*/}
{item.runStatus === 'RUN' && (
<div className={styles['flex-box']} onClick={() => setVisible(true)}>
<img src={'/icons/addIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />
<span style={{ color: '#A2A2AB' }}></span>
</div>
<div className={styles['flex-box']}>
)}
{/*环境配置*/}
<div className={styles['flex-box']} onClick={() => setVisible(true)}>
<img src={'/icons/envIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />
<span style={{ color: '#A2A2AB' }}></span>
</div>
{/*下架组件*/}
{item.runStatus === 'RUN' && (
<div className={`${styles['flex-box']} ${styles['custom-red']}`} onClick={() => {
setSelectedItem(item);
setShowOffSaleModal(true);
}}>
<img src={'/icons/removedIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />
<span style={{ color: '#A2A2AB' }}></span>
<span></span>
</div>
)}
{/*上架组件*/}
{item.runStatus === 'STOP' && (
<div className={`${styles['flex-box']} ${styles['custom-blue']}`}
onClick={() => componentOnSaleHandler(item)}>
<img src={'/icons/removedIcon.png'}
style={{ width: 16, height: 16, marginRight: 5, transform: 'rotate(180deg)' }} />
<span></span>
</div>
)}
{/*更多操作*/}
<div className={styles['flex-box']}>
<img src={'/icons/moreIcon.png'} style={{ width: 16, height: 16, marginRight: 5 }} />
<span style={{ color: '#A2A2AB' }}></span>
@ -89,15 +153,29 @@ const CollapseList = () => {
<Collapse
expandIconPosition={'right'}
bordered={false}
onChange={(key, keys, e) => {
// 当展开某个折叠面板时,记录当前展开的 item 数据
if (keys && keys.length > 0) {
// 获取最后一个展开的 key当前操作的 key
const expandedKey = keys[keys.length - 1];
const expandedIndex = parseInt(expandedKey, 10);
const currentItem = collapses[expandedIndex];
setExpandedItem(currentItem);
}
else {
// 所有面板都折叠时,清空 expandedItem
setExpandedItem(null);
}
}}
>
{collapses.map((item, index) => (
<CollapseItem
key={item.identifier}
header={headerNode(item)}
name="1"
extra={extraNode()}
name={index.toString()}
extra={extraNode(item)}
>
<ListNode />
<ListNode componentData={item} />
</CollapseItem>
))}
</Collapse>
@ -107,6 +185,33 @@ const CollapseList = () => {
visible={visible}
setVisible={setVisible}
/>
<Modal
title={'下架组件'}
visible={showOffSaleModal}
style={{ width: '45%' }}
onCancel={() => {
setSelectedItem(null);
}}
footer={[
<Button key="cancel" onClick={() => setShowOffSaleModal(false)}>
</Button>,
<Button key="offshelf" type="primary" onClick={() => {
componentOffSaleHandler(false);
}}>
</Button>,
<Button key="stopAndOffshelf" type="primary" status="danger" onClick={() => {
componentOffSaleHandler(true);
}}>
线
</Button>
]}
>
<p></p>
<p> 线使线</p>
</Modal>
</>
);
};

@ -1,21 +1,36 @@
import React from 'react';
import React, { useState } from 'react';
import styles from './style/index.module.less';
import { Button, Input, Radio, Space } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import CollapseList from './collapseList';
import { startStatusConstant } from '@/const/isdp/componentDeploy';
const ComponentDeployment = () => {
const [searchKeyword, setSearchKeyword] = useState('');
const [selectedStatus, setSelectedStatus] = useState<string | undefined>(undefined);
// 状态选项配置
const statusOptions = [
{ label: '全部', value: undefined },
{ label: '启用中', value: "RUN" },
{ label: '已下架', value: "STOP" }
];
return (
<div className={styles['component-deployment']}>
<div className={styles['header']}>
<Radio.Group defaultValue={'Beijing'} name="button-radio-group">
{['全部', '启用中', '已下架'].map((item) => {
<Radio.Group
value={selectedStatus}
onChange={(value) => setSelectedStatus(value)}
name="button-radio-group"
>
{statusOptions.map((item) => {
return (
<Radio key={item} value={item}>
<Radio key={item.label} value={item.value}>
{({ checked }) => {
return (
<Button tabIndex={-1} key={item} shape="round" type={checked ? 'primary' : 'default'}>
{item}
<Button tabIndex={-1} key={item.label} shape="round" type={checked ? 'primary' : 'default'}>
{item.label}
</Button>
);
}}
@ -29,6 +44,11 @@ const ComponentDeployment = () => {
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: 236 }}
value={searchKeyword}
onChange={(value) => setSearchKeyword(value)}
onPressEnter={() => {
// 触发搜索
}}
/>
<Button
type="primary"
@ -38,8 +58,8 @@ const ComponentDeployment = () => {
</Button>
</Space>
</div>
<div className="content">
<CollapseList></CollapseList>
<div className={styles['content']}>
<CollapseList searchKeyword={searchKeyword} runStatus={selectedStatus} />
</div>
</div>
);

@ -1,39 +1,72 @@
import React from "react";
import React, { useEffect, useState } from 'react';
import { Button, Space, Table, TableColumnProps } from '@arco-design/web-react';
import styles from '@/pages/componentDevelopment/componentDeployment/style/collapseList.module.less';
import { getInstanceList } from '@/api/componentInstance';
const ListNode = () => {
interface ListNodeProps {
componentData: any; // 组件数据
}
const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
// 获取实例列表
const fetchInstanceList = async () => {
if (!componentData?.identifier) return;
setLoading(true);
try {
const params = {
current: 1,
size: 10
};
const res: any = await getInstanceList(params, componentData.identifier);
if (res.code === 200) {
setData(res.data.records || []);
}
} catch (error) {
console.error('获取实例列表失败:', error);
} finally {
setLoading(false);
}
};
// 监听 componentData 变化,调用接口
useEffect(() => {
fetchInstanceList();
}, [componentData]);
const columns: TableColumnProps[] = [
{
title: '#',
dataIndex: 'key',
align: 'center'
align: 'center',
render: (col, record, index) => index + 1
},
{
title: '组件标识',
dataIndex: 'name',
dataIndex: 'identifier',
align: 'center'
},
{
title: '实例名称',
dataIndex: 'salary',
dataIndex: 'name',
align: 'center'
},
{
title: '运行类型',
dataIndex: 'address',
dataIndex: 'runType',
align: 'center'
},
{
title: '运行状态',
dataIndex: 'email',
dataIndex: 'runStatus',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'email',
dataIndex: 'createTime',
align: 'center'
},
{
@ -43,8 +76,6 @@ const ListNode = () => {
render: (col, record, index) => (
<div className={styles['table-handle-box']}>
<Space size={20}>
<Button type="text"></Button>
<Button type="text"></Button>
<Button type="text"></Button>
<Button type="text"></Button>
<Button type="text"
@ -67,46 +98,16 @@ const ListNode = () => {
)
}
];
const data = [
{
key: '1',
name: 'Jane Doe',
salary: 23000,
address: '32 Park Road, London',
email: 'jane.doe@example.com'
},
{
key: '2',
name: 'Alisa Ross',
salary: 25000,
address: '35 Park Road, London',
email: 'alisa.ross@example.com'
},
{
key: '3',
name: 'Kevin Sandra',
salary: 22000,
address: '31 Park Road, London',
email: 'kevin.sandra@example.com'
},
{
key: '4',
name: 'Ed Hellen',
salary: 17000,
address: '42 Park Road, London',
email: 'ed.hellen@example.com'
},
{
key: '5',
name: 'William Smith',
salary: 27000,
address: '62 Park Road, London',
email: 'william.smith@example.com'
}
];
return (
<Table pagination={false} border={false} columns={columns} data={data} />
)
}
<Table
loading={loading}
pagination={false}
border={false}
columns={columns}
data={data}
/>
);
};
export default ListNode;

@ -1,4 +1,36 @@
.collapse-list {
:global(.arco-collapse-item-header-extra) {
.flex-box {
display: flex;
align-items: center;
}
.custom-red {
color: #F53F3F;
img {
filter: brightness(0) saturate(100%) invert(38%) sepia(85%) saturate(3091%) hue-rotate(342deg) brightness(99%) contrast(93%);
}
span {
color: #F53F3F;
}
}
.custom-blue {
color: #3491FA;
img {
filter: brightness(0) saturate(100%) invert(48%) sepia(98%) saturate(1672%) hue-rotate(195deg) brightness(101%) contrast(98%);
}
span {
color: #3491FA;
}
}
}
:global(.arco-collapse-item-content-box) {
padding: 0;
}
@ -13,10 +45,6 @@
margin-right: 20px;
}
.flex-box {
display: flex;
align-items: center;
}
.table-handle-box {
:global(.arco-btn) {

@ -1,7 +1,9 @@
.component-deployment {
height: 98%;
height: 100%;
background-color: #ffffff;
padding: 17px 19px 0 24px;
display: flex;
flex-direction: column;
.header {
display: flex;
@ -9,5 +11,13 @@
padding-bottom: 15px;
border-bottom: 1px solid #ebebeb;
margin-bottom: 25px;
flex-shrink: 0;
}
.content {
flex: 1;
overflow: auto;
min-height: 0;
padding-bottom: 150px;
}
}
Loading…
Cancel
Save