feat: 对接虚拟化平台管理
This commit is contained in:
@@ -3,17 +3,18 @@
|
||||
<!-- 顶部信息 -->
|
||||
<div class="page-header" v-if="!embedded">
|
||||
<div class="header-left">
|
||||
<el-button @click="goBack" :icon="ArrowLeft">返回</el-button>
|
||||
<div class="header-info">
|
||||
<h3>宿主机组映射管理</h3>
|
||||
<span class="sub-info" v-if="serviceName">所属主控服务:{{ serviceName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" @click="handleSync" :loading="syncLoading">
|
||||
<el-select v-model="selectedServiceId" placeholder="选择主控服务" filterable style="width: 240px" @change="handleServiceChange">
|
||||
<el-option v-for="s in serviceOptions" :key="s.id" :label="`${s.name} (ID: ${s.id})`" :value="s.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSync" :loading="syncLoading" :disabled="!serviceId">
|
||||
<el-icon><RefreshRight /></el-icon>从远程同步
|
||||
</el-button>
|
||||
<el-button @click="loadHostGroups">
|
||||
<el-button @click="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -23,88 +24,46 @@
|
||||
<el-button @click="loadHostGroups"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 布局:左侧本地主机组列表 / 右侧详情&操作 -->
|
||||
<div class="content-layout">
|
||||
<!-- 左侧:本地主机组列表 -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<h4>本地主机组列表</h4>
|
||||
</div>
|
||||
<div class="panel-body" v-loading="loading">
|
||||
<el-table :data="hostGroupList" stripe style="width: 100%" @row-click="handleRowClick"
|
||||
:row-class-name="getRowClassName" highlight-current-row>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="远程ID" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.remoteId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="父级远程ID" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.parentRemoteId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定商品组" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.goodGroupId" type="success" size="small">
|
||||
商品组#{{ row.goodGroupId }}
|
||||
</el-tag>
|
||||
<span v-else class="text-muted">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定商品" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.goodId" type="warning" size="small">
|
||||
商品#{{ row.goodId }}
|
||||
</el-tag>
|
||||
<span v-else class="text-muted">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.note || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click.stop="handleEditGroup(row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click.stop="handleBind(row)">绑定</el-button>
|
||||
<el-button link type="success" size="small" @click.stop="handleGenerateGoods(row)">生成商品</el-button>
|
||||
<el-button link type="danger" size="small" @click.stop="handleDeleteGroup(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 本地主机组列表(树形折叠) -->
|
||||
<div class="main-panel">
|
||||
<div class="panel-header">
|
||||
<h4>本地主机组列表</h4>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:远程主机组树 -->
|
||||
<div class="right-panel">
|
||||
<div class="panel-header">
|
||||
<h4>远程主机组树</h4>
|
||||
<el-button size="small" @click="loadRemoteTree" :loading="remoteTreeLoading">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="panel-body" v-loading="remoteTreeLoading">
|
||||
<el-tree
|
||||
v-if="remoteTreeData.length > 0"
|
||||
:data="remoteTreeData"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node-content">
|
||||
<span class="tree-node-name">{{ data.name }}</span>
|
||||
<span class="tree-node-id">ID: {{ data.id }}</span>
|
||||
<span v-if="data.note" class="tree-node-note">{{ data.note }}</span>
|
||||
</div>
|
||||
<div class="panel-body" v-loading="loading">
|
||||
<el-table :data="treeGroupList" stripe style="width: 100%" row-key="_rowKey"
|
||||
:tree-props="{ children: '_children', hasChildren: '_hasChildren' }">
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column label="远程ID" width="80">
|
||||
<template #default="{ row }">{{ row.remoteId || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="父级远程ID" width="100">
|
||||
<template #default="{ row }">{{ row.parentRemoteId || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定商品组" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.goodGroupId" type="success" size="small">商品组#{{ row.goodGroupId }}</el-tag>
|
||||
<span v-else class="text-muted">未绑定</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<el-empty v-else description="暂无远程主机组数据,请先同步" />
|
||||
</div>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定商品" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.goodId" type="warning" size="small">商品#{{ row.goodId }}</el-tag>
|
||||
<span v-else class="text-muted">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click.stop="handleEditGroup(row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click.stop="handleBind(row)">绑定</el-button>
|
||||
<el-button link type="success" size="small" @click.stop="handleGenerateGoods(row)">生成商品</el-button>
|
||||
<el-button link type="danger" size="small" @click.stop="handleDeleteGroup(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -219,8 +178,9 @@ import {
|
||||
updateHostGroup,
|
||||
generateGoodsByHostGroup,
|
||||
deleteHostGroup,
|
||||
getRemoteHostGroupTree
|
||||
getKvmServiceList
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import ProductGroupSelector from '@/components/admin/ProductGroupSelector.vue'
|
||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -230,16 +190,76 @@ const router = useRouter()
|
||||
const embedded = inject('embedded', false)
|
||||
const injectedServiceId = inject('serviceId', null)
|
||||
const injectedServiceName = inject('serviceName', null)
|
||||
const serviceId = computed(() => injectedServiceId?.value || parseInt(route.query.service_id) || 0)
|
||||
const serviceName = computed(() => injectedServiceName?.value || route.query.service_name || '')
|
||||
|
||||
const selectedServiceId = ref(parseInt(route.query.service_id) || null)
|
||||
const serviceOptions = ref([])
|
||||
|
||||
const serviceId = computed(() => injectedServiceId?.value || selectedServiceId.value || 0)
|
||||
const serviceName = computed(() => {
|
||||
if (injectedServiceName?.value) return injectedServiceName.value
|
||||
const s = serviceOptions.value.find(x => x.id === selectedServiceId.value)
|
||||
return s?.name || route.query.service_name || ''
|
||||
})
|
||||
|
||||
const normalizeService = (s) => ({
|
||||
id: s.Id ?? s.id,
|
||||
name: s.Name ?? s.name,
|
||||
host: s.Host ?? s.host,
|
||||
port: s.Port ?? s.port,
|
||||
note: s.Note ?? s.note
|
||||
})
|
||||
|
||||
const loadServiceOptions = async () => {
|
||||
try {
|
||||
const res = await getKvmServiceList({ page: 1, count: 100, key: '' })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
serviceOptions.value = raw.map(normalizeService)
|
||||
}
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
const handleServiceChange = () => {
|
||||
hostGroupList.value = []
|
||||
if (serviceId.value) {
|
||||
loadHostGroups()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!serviceId.value) {
|
||||
ElMessage.warning('请先选择主控服务')
|
||||
return
|
||||
}
|
||||
loadHostGroups()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const syncLoading = ref(false)
|
||||
const remoteTreeLoading = ref(false)
|
||||
const hostGroupList = ref([])
|
||||
const remoteTreeData = ref([])
|
||||
const selectedGroup = ref(null)
|
||||
|
||||
const treeGroupList = computed(() => {
|
||||
const items = hostGroupList.value
|
||||
if (!items.length) return []
|
||||
const map = new Map()
|
||||
items.forEach(item => {
|
||||
map.set(item.remoteId, { ...item, _rowKey: `g-${item.id}`, _children: [], _hasChildren: false })
|
||||
})
|
||||
const roots = []
|
||||
map.forEach(item => {
|
||||
if (item.parentRemoteId && map.has(item.parentRemoteId)) {
|
||||
const parent = map.get(item.parentRemoteId)
|
||||
parent._children.push(item)
|
||||
parent._hasChildren = true
|
||||
} else {
|
||||
roots.push(item)
|
||||
}
|
||||
})
|
||||
return roots
|
||||
})
|
||||
|
||||
// 规范化后端 PascalCase 字段为前端 camelCase
|
||||
// 同时保留原始字段以便在需要时直接访问
|
||||
const normalizeHostGroup = (item) => {
|
||||
@@ -286,27 +306,6 @@ const loadHostGroups = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 远程主机组树 ==========
|
||||
const loadRemoteTree = async () => {
|
||||
if (!serviceId.value) return
|
||||
remoteTreeLoading.value = true
|
||||
try {
|
||||
const res = await getRemoteHostGroupTree({ service_id: serviceId.value })
|
||||
const body = res?.data
|
||||
const data = body?.data || body
|
||||
if (Array.isArray(data)) {
|
||||
remoteTreeData.value = data
|
||||
} else {
|
||||
remoteTreeData.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取远程主机组树失败:', error)
|
||||
ElMessage.error('获取远程主机组树失败')
|
||||
} finally {
|
||||
remoteTreeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 同步 ==========
|
||||
const handleSync = async () => {
|
||||
if (!serviceId.value) {
|
||||
@@ -330,24 +329,14 @@ const handleSync = async () => {
|
||||
ElMessage.warning(body?.message || '同步返回异常')
|
||||
}
|
||||
loadHostGroups()
|
||||
loadRemoteTree()
|
||||
} catch (error) {
|
||||
ElMessage.error('同步失败: ' + (error?.response?.data?.message || error.message))
|
||||
ElMessage.error(extractApiError(error?.response?.data, '同步失败'))
|
||||
} finally {
|
||||
syncLoading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// ========== 行点击选中 ==========
|
||||
const handleRowClick = (row) => {
|
||||
selectedGroup.value = row
|
||||
}
|
||||
|
||||
const getRowClassName = ({ row }) => {
|
||||
return selectedGroup.value?.id === row.id ? 'selected-row' : ''
|
||||
}
|
||||
|
||||
// ========== 编辑 ==========
|
||||
const editDialogVisible = ref(false)
|
||||
const editSubmitLoading = ref(false)
|
||||
@@ -388,10 +377,10 @@ const submitEdit = () => {
|
||||
editDialogVisible.value = false
|
||||
loadHostGroups()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '修改失败')
|
||||
ElMessage.error(extractApiError(body, '修改失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('修改失败: ' + (error?.response?.data?.message || error.message))
|
||||
ElMessage.error(extractApiError(error?.response?.data, '修改失败'))
|
||||
} finally {
|
||||
editSubmitLoading.value = false
|
||||
}
|
||||
@@ -465,10 +454,10 @@ const submitBind = async () => {
|
||||
bindDialogVisible.value = false
|
||||
loadHostGroups()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '绑定失败')
|
||||
ElMessage.error(extractApiError(body, '绑定失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('绑定失败: ' + (error?.response?.data?.message || error.message))
|
||||
ElMessage.error(extractApiError(error?.response?.data, '绑定失败'))
|
||||
} finally {
|
||||
bindSubmitLoading.value = false
|
||||
}
|
||||
@@ -523,10 +512,10 @@ const submitGenerate = () => {
|
||||
generateDialogVisible.value = false
|
||||
loadHostGroups()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '商品生成失败')
|
||||
ElMessage.error(extractApiError(body, '商品生成失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('生成失败: ' + (error?.response?.data?.message || error.message))
|
||||
ElMessage.error(extractApiError(error?.response?.data, '生成失败'))
|
||||
} finally {
|
||||
generateSubmitLoading.value = false
|
||||
}
|
||||
@@ -553,10 +542,10 @@ const handleDeleteGroup = (row) => {
|
||||
ElMessage.success('删除成功')
|
||||
loadHostGroups()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '删除失败')
|
||||
ElMessage.error(extractApiError(body, '删除失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败: ' + (error?.response?.data?.message || error.message))
|
||||
ElMessage.error(extractApiError(error?.response?.data, '删除失败'))
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
@@ -567,9 +556,9 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!embedded) loadServiceOptions()
|
||||
if (serviceId.value) {
|
||||
loadHostGroups()
|
||||
loadRemoteTree()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -617,21 +606,7 @@ onMounted(() => {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 布局 */
|
||||
.content-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.content-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
.right-panel {
|
||||
.main-panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
@@ -658,41 +633,6 @@ onMounted(() => {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 表格行选中高亮 */
|
||||
:deep(.el-table .selected-row) {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
/* 远程树节点 */
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.tree-node-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.tree-node-id {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
background: #f0f2f5;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tree-node-note {
|
||||
font-size: 11px;
|
||||
color: #b0b3b8;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
|
||||
Reference in New Issue
Block a user