You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<list-header description="资源池管理用于统一管理和调度计算资源,支持资源的动态分配、回收与负载均衡,提升资源利用率和系统灵活性。">
<template #actions>
<el-button @click="editId = null; dialogVisible = true" style="margin-right: 24px;" type="primary"
round>创建资源池</el-button>
</template>
</list-header>
<div v-loading="loading" style="min-height: 90px;">
<block-box
v-for="{ poolId, poolName, poolType, nodeNum, cpuCores, gpuNum, availableMemory, totalMemory, diskSize, nodeList, linkUrl }, index in paginatedList"
:key="poolId" style="margin: 15px 0 0 0;">
<el-row style="align-items: center;">
<div class="left">
<div style="display: flex;align-items: center;">
<b class="title" style="margin-right: 10px;">{{ poolName }}</b>
<el-tag disable-transitions :class="getPoolTypeClass(poolType)">
{{ ['单机单卡', '单机多卡', '多机多卡'][poolType] || '未知类型' }}
</el-tag>
</div>
<div class="tags">
<span>节点数量&nbsp;&nbsp;{{ nodeNum }}</span>
<span>CPU数&nbsp;&nbsp;{{ cpuCores }}核</span>
<span>显卡数量&nbsp;&nbsp;{{ gpuNum }}张</span>
<span>可用/总内存&nbsp;&nbsp;{{ bytesToGB(availableMemory) }}GB / {{ bytesToGB(totalMemory) }}GB</span>
<span>磁盘大小&nbsp;&nbsp;{{ bytesToGB(diskSize) }}GB</span>
</div>
</div>
<div class="right">
<el-button @click="sendRouteChange(`/admin/vgpu/poll/admin/${poolId}?poolName=${poolName}`)"
type="text">查看详情</el-button>
<template v-if="index === 0 && currentPage === 1">
<el-button @click="sendRouteChange(linkUrl, 'open')" type="text">配置</el-button>
</template>
<template v-else>
<el-button @click="handleEdit(poolId, poolName, nodeList, poolType)" type="text">编辑</el-button>
<el-button @click="() => handleDelete(poolId)" type="text">删除</el-button>
</template>
</div>
</el-row>
</block-box>
</div>
<!-- 分页组件 -->
<el-pagination style="margin-top: 15px;" background :current-page="currentPage" :page-size="pageSize"
layout="total, ->, sizes, jumper, prev, next" :page-sizes="[10, 20, 50, 100]" :total="list.length"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
<el-dialog @close="editId = null; input = ''; poolType = null; nodeSelect = []" v-model="dialogVisible"
:title="editId ? '编辑资源池' : '创建资源池'" width="1180" :before-close="handleClose">
<el-row :wrap="false" style="align-items: center;">
<span style="flex-shrink: 0; margin-right: 14px;">资源池名称</span>
<el-input style="flex: 1;" v-model="input" size="large" maxlength="20" />
</el-row>
<el-row :wrap="false" style="align-items: center; margin-top: 20px;">
<span style="flex-shrink: 0; margin-right: 14px;">资源池类型</span>
<el-select v-model="poolType" size="large" style="flex: 1;" placeholder="请选择资源池类型" @change="handleTypeChange">
<el-option label="单机单卡" value="0" />
<el-option label="单机多卡" value="1" />
<el-option label="多机多卡" value="2" />
</el-select>
</el-row>
<div v-if="poolType" style="margin-top: 20px; margin-bottom: 10px;">
<span>选择节点</span>
<span style="float: right;">已选<span style="color: #3061D0; margin: 0 5px;">{{ nodeSelect.length
}}</span>个节点</span>
</div>
<div v-if="poolType" class="wrap">
<div class="wrap-left">
<div style="margin-top: 12px;"
v-for="{ nodeIp, cpuCores, gpuNum, gpuMemory, totalMemory, diskSize } in filteredNodeList"
:key="nodeIp">
<div style="display: flex; align-items: center;">
<el-checkbox v-if="poolType === '2'" :model-value="nodeSelect.includes(nodeIp)"
@change="handleCheckboxChange(nodeIp)" />
<el-radio v-else :model-value="nodeSelect[0]" :label="nodeIp" @change="handleRadioChange(nodeIp)" />
<span style="color: #0B1524; margin-left: 8px;">{{ nodeIp }}</span>
</div>
<div class="wrap-row">
<div>显卡数量<span>{{ gpuNum }}张</span></div>
<div>显卡大小<span>{{ bytesToGB(gpuMemory) }}GB</span></div>
<div>内存大小<span>{{ bytesToGB(totalMemory) }}GB</span></div>
<div>磁盘大小<span>{{ bytesToGB(diskSize) }}GB</span></div>
<div>CPU<span>{{ cpuCores }}核</span></div>
</div>
</div>
</div>
<div class="wrap-center">
<svg-icon icon="arrow" />
</div>
<div class="wrap-right">
<div style="margin-top: 12px;"
v-for="{ nodeIp, cpuCores, gpuNum, gpuMemory, totalMemory, diskSize } in nodeList.filter(e => nodeSelect.includes(e.nodeIp))"
:key="nodeIp">
<div style="display: flex; align-items: center;">
<el-icon :size="16" color="red" style="cursor: pointer;" @click="handleCheckboxChange(nodeIp)">
<Remove />
</el-icon>
<span style="color: #0B1524; margin-left: 6px;">{{ nodeIp }}</span>
</div>
<div class="wrap-row" style="margin-top: 6px;">
<div>显卡数量<span>{{ gpuNum }}</span></div>
<div>显卡大小<span>{{ bytesToGB(gpuMemory) }}GB</span></div>
<div>内存大小<span>{{ bytesToGB(totalMemory) }}GB</span></div>
<div>磁盘大小<span>{{ bytesToGB(diskSize) }}GB</span></div>
<div>CPU<span>{{ cpuCores }}</span></div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button :loading="btnLoading" type="primary" @click="handleOk"></el-button>
</template>
</el-dialog>
</template>
<script setup lang="jsx">
import pollApi from '~/vgpu/api/poll';
import { ref, onMounted, computed, watchEffect } from 'vue';
import BlockBox from '@/components/BlockBox.vue';
import { Remove } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bytesToGB } from '@/utils';
import useParentAction from '~/vgpu/hooks/useParentAction';
const { sendRouteChange } = useParentAction();
// 数据列表相关
const list = ref([])
const loading = ref(true)
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 对话框相关
const dialogVisible = ref(false)
const editId = ref(null)
const input = ref('')
const poolType = ref(null) // 默认不选择
const btnLoading = ref(false)
// 节点选择相关
const nodeList = ref([])
const nodeSelect = ref([])
const getPoolTypeClass = (type) => {
return {
'tag-single-single': type === 0,
'tag-single-multi': type === 1,
'tag-multi-multi': type === 2
};
}
// 计算分页后的数据
const paginatedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return list.value.slice(start, end)
})
// 根据类型过滤节点列表
const filteredNodeList = computed(() => {
if (!poolType.value) return []
if (poolType.value === '1') {
// 单机多卡过滤掉只有1张显卡的节点
return nodeList.value.filter(node => node.gpuNum > 1)
}
// 单机单卡(0)和多机多卡(2):显示所有节点
return nodeList.value
})
// 分页大小变化
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1 // 切换每页条数时重置到第一页
}
// 当前页变化
const handleCurrentChange = (val) => {
currentPage.value = val
}
// 确认操作
const handleOk = async () => {
if (!input.value) {
ElMessage({
message: '请输入资源池名称',
type: 'warning',
})
return;
}
if (!poolType.value) {
ElMessage({
message: '请选择资源池类型',
type: 'warning',
})
return;
}
if (!nodeSelect.value.length) {
ElMessage({
message: '请选择节点',
type: 'warning',
})
return;
}
btnLoading.value = true;
try {
const nodes = nodeList.value.filter(e => nodeSelect.value.includes(e.nodeIp)).map(e => ({ nodeIp: e.nodeIp, nodeName: e.nodeName }))
let res;
if (editId.value) {
res = await pollApi.update({
pool_id: editId.value,
pool_name: input.value,
pool_type: poolType.value,
nodes
})
} else {
res = await pollApi.create({
pool_name: input.value,
pool_type: poolType.value,
nodes
})
}
if (res?.code === 200) {
getList();
dialogVisible.value = false;
}
} finally {
btnLoading.value = false;
}
btnLoading.value = false;
}
// 编辑处理
const handleEdit = (poolId, poolName, nodes, type) => {
dialogVisible.value = true;
editId.value = poolId;
input.value = poolName;
nodeSelect.value = nodes.map(e => e.nodeIp);
// 设置资源池类型
poolType.value = type.toString();
}
// 类型变化处理
const handleTypeChange = () => {
// 类型改变时清空已选择的节点
nodeSelect.value = [];
}
// 单选变化
const handleRadioChange = (ip) => {
nodeSelect.value = [ip];
}
// 复选框变化
const handleCheckboxChange = (ip) => {
const index = nodeSelect.value.indexOf(ip);
if (index > -1) {
nodeSelect.value.splice(index, 1);
} else {
nodeSelect.value.push(ip);
}
}
// 删除操作
const handleDelete = async (id) => {
ElMessageBox.confirm(`确定要删除当前资源池吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
beforeClose: async (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
const res = await pollApi.delete({ pool_id: id });
if (res.code === 200) {
ElMessage.success('删除成功');
getList();
done();
}
instance.confirmButtonLoading = false;
} else {
done();
}
}
})
}
// 获取资源池列表
const getList = async () => {
loading.value = true
const res = await pollApi.getPollList()
loading.value = false
list.value = res.data
total.value = res.data.length
}
// 初始化加载数据
onMounted(async () => {
getList();
const res = await pollApi.getNodeList()
nodeList.value = res.data
});
</script>
<style scoped lang="scss">
.left {
flex: 1;
}
.right {
width: 170px;
display: flex;
justify-content: end;
}
.title {
color: #3D4F62;
font-size: 18px;
}
.tag-single-single {
background: rgba(50,229,143,0.2);
border-radius: 4px;
border: 1px solid #82EAC1;
color: #1AB274;
font-size: 14px;
}
.tag-single-multi {
background: rgba(229,149,50,0.1);
border-radius: 4px;
border: 1px solid #F5D6BA;
color: #F38824;
font-size: 14px;
}
.tag-multi-multi {
background: rgba(113,195,252,0.2);
border-radius: 4px;
border: 1px solid #BBC8EE;
color: #406EE2;
font-size: 14px;
}
.tags {
display: flex;
gap: 60px;
margin-top: 10px;
span {
font-size: 14px;
color: #3D4F62;
}
}
.wrap {
display: flex;
align-items: center;
}
.wrap-left,
.wrap-right {
flex: 1;
height: 320px;
background: #F6F7F9;
border-radius: 4px;
overflow: auto;
padding: 0 14px 14px 14px;
.wrap-row {
display: flex;
gap: 18px;
font-size: 12px;
margin-left: 23px;
div {
color: #5A6B7D;
span {
color: #0B1524;
margin-left: 4px;
}
}
}
}
.wrap-center {
width: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>