+
-
-
-
-
-
-
-
-
- {{ data.name }}
- ID: {{ data.id }}
- {{ data.note }}
-
+
+
+
+
+
+ {{ row.remoteId || '-' }}
+
+
+ {{ row.parentRemoteId || '-' }}
+
+
+
+ 商品组#{{ row.goodGroupId }}
+ 未绑定
-
-
-
+
+
+
+ 商品#{{ row.goodId }}
+ 未绑定
+
+
+
+ {{ row.note || '-' }}
+
+
+
+ 编辑
+ 绑定
+ 生成商品
+ 删除
+
+
+
@@ -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()
}
})
@@ -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;
diff --git a/src/views/virtualization/HostManage.vue b/src/views/virtualization/HostManage.vue
index 5050ffc..4320fab 100644
--- a/src/views/virtualization/HostManage.vue
+++ b/src/views/virtualization/HostManage.vue
@@ -52,7 +52,7 @@
CPU: {{ row.max_cpu }}核
- 内存: {{ formatMemMB(row.max_memory) }}
+ 内存: {{ formatMemKB(row.max_memory) }}
磁盘: {{ formatDiskGB(row.max_disk) }}
@@ -95,7 +95,7 @@