feat: 对接虚拟化平台管理
Build and Deploy Vue3 / build (push) Successful in 1m22s
Build and Deploy Vue3 / deploy (push) Successful in 1m2s

This commit is contained in:
2026-03-19 18:13:24 +08:00
parent cd16ec17ae
commit cf19956b88
24 changed files with 5000 additions and 807 deletions
+174 -104
View File
@@ -5,7 +5,7 @@
<el-button @click="goBack" :icon="ArrowLeft">返回</el-button>
<div class="header-info">
<h3>虚拟机管理</h3>
<span class="sub-info" v-if="serviceName">主控服务{{ serviceName }} | 宿主机{{ selectedHostName || '请选择' }}</span>
<span class="sub-info" v-if="serviceName">主控服务{{ serviceName }}</span>
</div>
</div>
<div class="header-right">
@@ -23,9 +23,6 @@
<el-input v-model="keyword" placeholder="搜索虚拟机" clearable style="width: 220px" @keyup.enter="handleSearch" @clear="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-select v-model="hostIdInput" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="handleSearch">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
<el-select v-model="filterStatus" placeholder="状态" clearable style="width: 130px" @change="handleSearch">
<el-option v-for="s in vmStatuses" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
@@ -77,14 +74,9 @@
</div>
<!-- 创建弹窗 -->
<el-dialog v-model="createDialogVisible" title="创建虚拟机" width="640px" destroy-on-close>
<el-dialog v-model="createDialogVisible" title="创建虚拟机" width="800px" destroy-on-close>
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<el-form-item label="名称"><el-input v-model="createForm.name" placeholder="不填随机生成" /></el-form-item>
<el-form-item label="宿主机" prop="host_id">
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
</el-form-item>
<el-form-item label="镜像" prop="image_id">
<div class="bind-selector-row">
<el-input :model-value="createForm.image_id ? `镜像 #${createForm.image_id}${createForm._imageName ? ' - ' + createForm._imageName : ''}` : '未选择'" disabled style="flex: 1" />
@@ -92,46 +84,82 @@
<el-button v-if="createForm.image_id" @click="createForm.image_id = 0; createForm._imageName = ''" style="margin-left: 4px">清除</el-button>
</div>
</el-form-item>
<el-divider content-position="left">资源配置</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="CPU()" prop="vcpu">
<el-input-number v-model="createForm.vcpu" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="内存(KB)" prop="memory">
<el-input-number v-model="createForm.memory" :min="65536" :step="65536" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="系统盘(MB)" prop="system_size">
<el-input-number v-model="createForm.system_size" :min="1024" :step="1024" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="下行带宽(Mbps)">
<el-input-number v-model="createForm.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="上行带宽(Mbps)">
<el-input-number v-model="createForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">可选配置</el-divider>
<el-form-item label="宿主机组">
<el-form-item label="用户" prop="user_id">
<div class="bind-selector-row">
<el-input :model-value="createForm.host_group_id ? `宿主机组 #${createForm.host_group_id}${createForm._groupName ? ' - ' + createForm._groupName : ''}` : '未选择'" disabled style="flex: 1" />
<el-input :model-value="createForm.user_id ? `${createForm._userName || ''} (ID: ${createForm.user_id})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showUserSelector = true" style="margin-left: 8px">选择</el-button>
<el-button v-if="createForm.user_id" @click="createForm.user_id = 0; createForm._userName = ''" style="margin-left: 4px">清除</el-button>
</div>
</el-form-item>
<el-divider content-position="left">宿主机配置(二选一)</el-divider>
<el-form-item label="分配方式">
<el-radio-group v-model="hostMode">
<el-radio value="host">指定宿主机</el-radio>
<el-radio value="group">指定宿主机组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="宿主机" v-if="hostMode === 'host'">
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%" @change="(v) => loadNetworkOptions(v)">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
</el-form-item>
<el-form-item label="宿主机组" v-if="hostMode === 'group'">
<div class="bind-selector-row">
<el-input :model-value="createForm.host_group_id ? `${createForm._groupName || ''} (ID: ${createForm.host_group_id})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showHostGroupSelector = true" style="margin-left: 8px">选择</el-button>
<el-button v-if="createForm.host_group_id" @click="createForm.host_group_id = 0; createForm._groupName = ''" style="margin-left: 4px">清除</el-button>
</div>
</el-form-item>
<el-form-item label="用户ID"><el-input-number v-model="createForm.user_id" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
<el-form-item label="IP 数量"><el-input-number v-model="createForm.ip_num" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
<el-divider content-position="left">资源配置</el-divider>
<div class="resource-row">
<div class="resource-item">
<span class="resource-label">* 内存</span>
<el-select v-model="memoryUnit" class="resource-unit-select">
<el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
</el-select>
<el-input-number v-model="memoryDisplay" :min="1" controls-position="right" class="resource-input" />
</div>
<div class="resource-item">
<span class="resource-label">* 系统盘</span>
<el-select v-model="diskUnit" class="resource-unit-select">
<el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
</el-select>
<el-input-number v-model="diskDisplay" :min="1" controls-position="right" class="resource-input" />
</div>
</div>
<div class="resource-row">
<div class="resource-item">
<span class="resource-label">* CPU(核)</span>
<el-input-number v-model="createForm.vcpu" :min="1" controls-position="right" class="resource-input" />
</div>
<div class="resource-item">
<span class="resource-label">下行带宽(Mbps)</span>
<el-input-number v-model="createForm.rx_bandwidth" :min="0" controls-position="right" class="resource-input" />
</div>
<div class="resource-item">
<span class="resource-label">上行带宽(Mbps)</span>
<el-input-number v-model="createForm.tx_bandwidth" :min="0" controls-position="right" class="resource-input" />
</div>
</div>
<el-divider content-position="left">网络配置(二选一)</el-divider>
<el-form-item label="IP分配方式">
<el-radio-group v-model="ipMode">
<el-radio value="num">按IP数量分配</el-radio>
<el-radio value="ids">选择网络IP</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="IP数量" v-if="ipMode === 'num'">
<el-input-number v-model="createForm.ip_num" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="网络IP列表" v-if="ipMode === 'ids'">
<el-select v-model="createForm.network_ids" multiple filterable placeholder="选择网络IP" style="width: 100%">
<el-option v-for="n in networkOptions" :key="n.id" :label="`${n.name || ''} - ${n.address || n.ip || ''} (ID: ${n.id})`" :value="n.id" />
</el-select>
<div class="form-tip" v-if="!networkOptions.length">请先选择宿主机以加载可用网络</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
@@ -228,22 +256,17 @@
<template v-if="vmMetricsData">
<h4 style="margin: 16px 0 8px">实时指标</h4>
<el-descriptions :column="2" border size="small">
<template v-if="vmMetricsData.cpu">
<el-descriptions-item label="CPU使用率">{{ (vmMetricsData.cpu.cpu_usage_percent ?? 0).toFixed(1) }}%</el-descriptions-item>
</template>
<template v-if="vmMetricsData.memory">
<el-descriptions-item label="内存总量">{{ formatBytesRaw(vmMetricsData.memory.total) }}</el-descriptions-item>
<el-descriptions-item label="内存使用">{{ formatBytesRaw(vmMetricsData.memory.used) }} ({{ vmMetricsData.memory.percent || 0 }}%)</el-descriptions-item>
</template>
<template v-if="vmMetricsData.disk">
<el-descriptions-item v-for="(info, path) in vmMetricsData.disk" :key="path" :label="'磁盘 ' + path">
{{ formatBytesRaw(info.used) }} / {{ formatBytesRaw(info.total) }} ({{ info.percent || 0 }}%)
<el-descriptions-item label="虚拟机">{{ vmMetricsData.vm_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="CPU使用率">
<span :style="{ color: vmMetricsData.cpu_usage_percent > 90 ? '#F56C6C' : vmMetricsData.cpu_usage_percent > 60 ? '#E6A23C' : '#67C23A' }">
{{ (vmMetricsData.cpu_usage_percent ?? 0).toFixed(1) }}%
</span>
</el-descriptions-item>
<template v-if="vmMetricsData.internet_speed && Object.keys(vmMetricsData.internet_speed).length">
<el-descriptions-item label="网络速率" :span="2">
<div v-for="(val, key) in vmMetricsData.internet_speed" :key="key">{{ key }}: {{ val }}</div>
</el-descriptions-item>
</template>
<template v-if="vmMetricsData.network">
<el-descriptions-item label="网络接收">{{ formatBytesRaw(vmMetricsData.network.rx_bytes) }}</el-descriptions-item>
<el-descriptions-item label="网络发送">{{ formatBytesRaw(vmMetricsData.network.tx_bytes) }}</el-descriptions-item>
</template>
</el-descriptions>
</template>
</div>
@@ -256,6 +279,8 @@
<ImageSelectorPopup v-model="showRebuildImageSelector" :service-id="serviceId" :current-id="rebuildImageId" @confirm="handleRebuildImageSelected" />
<!-- 宿主机组选择器 -->
<HostGroupSelectorPopup v-model="showHostGroupSelector" :service-id="serviceId" :current-id="createForm.host_group_id" @confirm="handleHostGroupSelected" />
<!-- 用户选择器 -->
<UserListSelector v-model="showUserSelector" :current-user-id="createForm.user_id" @confirm="handleUserSelected" />
</div>
</template>
@@ -267,10 +292,12 @@ import { Plus, Refresh, Search, ArrowLeft, ArrowDown } from '@element-plus/icons
import {
getRemoteHostList, getVmList, getVmDetail, getVmStatus, getVmMetrics,
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
resumeVm, rescueVm, exitRescueVm, deleteVm
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
import UserListSelector from '@/components/admin/UserListSelector.vue'
const route = useRoute()
const router = useRouter()
@@ -288,7 +315,6 @@ const vmList = ref([])
const total = ref(0)
const keyword = ref('')
const filterStatus = ref('')
const hostIdInput = ref(0)
const hostOptions = ref([])
const queryParams = reactive({ page: 1, count: 10 })
@@ -296,12 +322,50 @@ const queryParams = reactive({ page: 1, count: 10 })
const showCreateImageSelector = ref(false)
const showRebuildImageSelector = ref(false)
const showHostGroupSelector = ref(false)
const showUserSelector = ref(false)
const selectedHostName = computed(() => {
const h = hostOptions.value.find(x => x.id === hostIdInput.value)
return h ? `${h.name} (${h.ip || h.id})` : (hostIdInput.value || '')
// 创建表单模式切换
const hostMode = ref('host')
const ipMode = ref('num')
const networkOptions = ref([])
// 内存单位: API传输单位为 KB
const memoryUnitOptions = [
{ label: 'KB', factor: 1 },
{ label: 'MB', factor: 1024 },
{ label: 'GB', factor: 1048576 }
]
const memoryUnit = ref('KB')
const getMemFactor = () => memoryUnitOptions.find(u => u.label === memoryUnit.value)?.factor || 1
const memoryDisplay = computed({
get: () => Math.round(createForm.memory / getMemFactor()),
set: (v) => { createForm.memory = Math.round(v * getMemFactor()) }
})
// 系统盘单位: API传输单位为 MB
const diskUnitOptions = [
{ label: 'MB', factor: 1 },
{ label: 'GB', factor: 1024 }
]
const diskUnit = ref('GB')
const getDiskFactor = () => diskUnitOptions.find(u => u.label === diskUnit.value)?.factor || 1
const diskDisplay = computed({
get: () => Math.round(createForm.system_size / getDiskFactor()),
set: (v) => { createForm.system_size = Math.round(v * getDiskFactor()) }
})
const loadNetworkOptions = async (hostId) => {
if (!hostId) return
try {
const res = await getNetworkList({ service_id: serviceId.value, host_id: hostId, page: 1, page_size: 200 })
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
networkOptions.value = inner.networks || inner.data || (Array.isArray(inner) ? inner : [])
}
} catch { networkOptions.value = [] }
}
const getHostLabel = (hid) => {
const h = hostOptions.value.find(x => x.id === hid)
return h ? `${h.name}` : (hid || '-')
@@ -340,16 +404,16 @@ const vmMetricsData = ref(null)
const createForm = reactive({
name: '', host_id: 0, image_id: 0, vcpu: 1, memory: 1048576,
system_size: 10240, rx_bandwidth: 0, tx_bandwidth: 0,
host_group_id: 0, user_id: 0, ip_num: 0,
_imageName: '', _groupName: ''
host_group_id: 0, user_id: 0, ip_num: 0, network_ids: [],
_imageName: '', _groupName: '', _userName: ''
})
const createRules = {
host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }],
image_id: [{ required: true, message: '请选择镜像', trigger: 'blur', type: 'number', min: 1 }],
vcpu: [{ required: true, message: '请输入CPU核数', trigger: 'blur' }],
memory: [{ required: true, message: '请输入内存(KB)', trigger: 'blur' }],
system_size: [{ required: true, message: '请输入系统盘(MB)', trigger: 'blur' }]
system_size: [{ required: true, message: '请输入系统盘大小', trigger: 'blur' }],
user_id: [{ required: true, message: '请选择用户', trigger: 'change', type: 'number', min: 1 }]
}
const vmStatusType = (s) => ({
@@ -397,14 +461,13 @@ const formatBytesRaw = (val) => {
const handleCreateImageSelected = (img) => { createForm.image_id = img.id; createForm._imageName = img.name }
const handleRebuildImageSelected = (img) => { rebuildImageId.value = img.id; rebuildImageName.value = img.name }
const handleHostGroupSelected = (group) => { createForm.host_group_id = group.id; createForm._groupName = group.name || '' }
const handleUserSelected = (user) => { createForm.user_id = user.user_id || user.id; createForm._userName = user.user_name || user.name || '' }
const loadList = async () => {
if (!serviceId.value) return
const hid = hostIdInput.value || hostId.value
if (!hid) { ElMessage.warning('请先选择宿主机'); return }
loading.value = true
try {
const params = { service_id: serviceId.value, host_id: hid, page: queryParams.page, count: queryParams.count }
const params = { service_id: serviceId.value, page: queryParams.page, count: queryParams.count }
if (keyword.value) params.key = keyword.value
if (filterStatus.value) params.status = filterStatus.value
const res = await getVmList(params)
@@ -414,41 +477,51 @@ const loadList = async () => {
vmList.value = inner.data || inner.vms || (Array.isArray(inner) ? inner : [])
total.value = inner.meta?.count ?? inner.all_count ?? inner.total ?? vmList.value.length
} else { vmList.value = []; total.value = 0 }
} catch (e) { ElMessage.error('获取虚拟机列表失败') } finally { loading.value = false }
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取虚拟机列表失败')) } finally { loading.value = false }
}
const handleSearch = () => { queryParams.page = 1; loadList() }
const handleAdd = () => {
Object.assign(createForm, {
name: '', host_id: hostIdInput.value || hostId.value || 0, image_id: 0,
name: '', host_id: hostId.value || 0, image_id: 0,
vcpu: 1, memory: 1048576, system_size: 10240,
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, user_id: 0, ip_num: 0,
_imageName: '', _groupName: ''
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, user_id: 0, ip_num: 0, network_ids: [],
_imageName: '', _groupName: '', _userName: ''
})
hostMode.value = 'host'
ipMode.value = 'num'
if (createForm.host_id) loadNetworkOptions(createForm.host_id)
createDialogVisible.value = true
}
const submitCreate = () => {
if (hostMode.value === 'host' && !createForm.host_id) { ElMessage.warning('请选择宿主机'); return }
if (hostMode.value === 'group' && !createForm.host_group_id) { ElMessage.warning('请选择宿主机组'); return }
if (ipMode.value === 'ids' && !createForm.network_ids.length) { ElMessage.warning('请选择网络IP'); return }
if (ipMode.value === 'num' && !createForm.ip_num) { ElMessage.warning('请输入IP数量'); return }
createFormRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const payload = {
service_id: serviceId.value,
host_id: createForm.host_id, image_id: createForm.image_id,
vcpu: createForm.vcpu, memory: createForm.memory, system_size: createForm.system_size
image_id: createForm.image_id,
vcpu: createForm.vcpu, memory: createForm.memory, system_size: createForm.system_size,
user_id: createForm.user_id
}
if (createForm.name) payload.name = createForm.name
if (createForm.rx_bandwidth) payload.rx_bandwidth = createForm.rx_bandwidth
if (createForm.tx_bandwidth) payload.tx_bandwidth = createForm.tx_bandwidth
if (createForm.host_group_id) payload.host_group_id = createForm.host_group_id
if (createForm.user_id) payload.user_id = createForm.user_id
if (createForm.ip_num) payload.ip_num = createForm.ip_num
if (hostMode.value === 'host') payload.host_id = createForm.host_id
else payload.host_group_id = createForm.host_group_id
if (ipMode.value === 'num') payload.ip_num = createForm.ip_num
else payload.network_ids = createForm.network_ids
const res = await createVm(payload)
if (res?.data?.code === 200) { ElMessage.success('创建成功'); createDialogVisible.value = false; loadList() }
else ElMessage.error(res?.data?.message || '创建失败')
} catch (e) { ElMessage.error('创建失败: ' + (e?.response?.data?.message || e.message)) }
else ElMessage.error(extractApiError(res?.data, '创建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
finally { submitLoading.value = false }
})
}
@@ -473,8 +546,8 @@ const handlePower = (row, action) => {
res = await apis[action](payload)
}
if (res?.data?.code === 200) { ElMessage.success(`${labels[action]}成功`); loadList() }
else ElMessage.error(res?.data?.message || `${labels[action]}失败`)
} catch (e) { ElMessage.error(`${labels[action]}失败`) }
else ElMessage.error(extractApiError(res?.data, `${labels[action]}失败`))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${labels[action]}失败`)) }
}).catch(() => {})
}
@@ -497,8 +570,8 @@ const submitRebuild = async () => {
try {
const res = await rebuildVm({ service_id: serviceId.value, vm_id: rebuildTarget.value.id, image_id: rebuildImageId.value })
if (res?.data?.code === 200) { ElMessage.success('重建成功'); rebuildDialogVisible.value = false; loadList() }
else ElMessage.error(res?.data?.message || '重建失败')
} catch (e) { ElMessage.error('重建失败') } finally { submitLoading.value = false }
else ElMessage.error(extractApiError(res?.data, '重建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重建失败')) } finally { submitLoading.value = false }
}
const handleRescue = (row) => {
@@ -511,8 +584,8 @@ const handleRescue = (row) => {
fd.append('vm_id', row.id)
const res = await rescueVm(fd)
if (res?.data?.code === 200) { ElMessage.success('已进入救援模式'); loadList() }
else ElMessage.error(res?.data?.message || '操作失败')
} catch (e) { ElMessage.error('操作失败') }
else ElMessage.error(extractApiError(res?.data, '操作失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
}).catch(() => {})
}
@@ -526,8 +599,8 @@ const handleExitRescue = (row) => {
fd.append('vm_id', row.id)
const res = await exitRescueVm(fd)
if (res?.data?.code === 200) { ElMessage.success('已退出救援模式'); loadList() }
else ElMessage.error(res?.data?.message || '操作失败')
} catch (e) { ElMessage.error('操作失败') }
else ElMessage.error(extractApiError(res?.data, '操作失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
}).catch(() => {})
}
@@ -554,7 +627,7 @@ const fetchVmStatus = async (vm) => {
currentDetail.value = { ...currentDetail.value, status: statusData.status ?? statusData }
ElMessage.success('状态已刷新: ' + vmStatusLabel(currentDetail.value.status))
}
} catch { ElMessage.error('获取状态失败') }
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取状态失败')) }
}
const fetchVmMetrics = async (vm) => {
@@ -562,7 +635,7 @@ const fetchVmMetrics = async (vm) => {
const res = await getVmMetrics({ service_id: serviceId.value, vm_name: vm.name })
if (res?.data?.code === 200) vmMetricsData.value = res.data.data?.data ?? res.data.data
else ElMessage.warning('暂无指标数据')
} catch { ElMessage.error('获取指标失败') }
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取指标失败')) }
}
const handleGoDetail = (row) => {
@@ -574,13 +647,10 @@ const handleDelete = (row) => {
confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', row.id)
const res = await deleteVm(fd)
const res = await deleteVm({ service_id: serviceId.value, vm_id: row.id })
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadList() }
else ElMessage.error(res?.data?.message || '删除失败')
} catch (e) { ElMessage.error('删除失败') }
else ElMessage.error(extractApiError(res?.data, '删除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
}).catch(() => {})
}
@@ -589,12 +659,7 @@ const goBack = () => { router.push('/virtualization/kvm-service') }
onMounted(async () => {
if (serviceId.value) {
await loadHostOptions()
if (hostId.value) {
hostIdInput.value = hostId.value
} else if (hostOptions.value.length > 0) {
hostIdInput.value = hostOptions.value[0].id
}
if (hostIdInput.value) loadList()
loadList()
}
})
</script>
@@ -615,4 +680,9 @@ onMounted(async () => {
.bind-selector-row { display: flex; align-items: center; width: 100%; }
:deep(.el-table) { --el-table-header-bg-color: #fafafa; }
:deep(.el-table th) { font-weight: 600; color: #303133; font-size: 13px; }
.resource-row { display: flex; gap: 20px; margin-bottom: 18px; }
.resource-item { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.resource-label { white-space: nowrap; font-size: 14px; color: #606266; flex-shrink: 0; }
.resource-unit-select { width: 72px; flex-shrink: 0; }
.resource-input { flex: 1; min-width: 0; }
</style>