diff --git a/src/views/virtualization/HostDetail.vue b/src/views/virtualization/HostDetail.vue index 979fb67..959ad84 100644 --- a/src/views/virtualization/HostDetail.vue +++ b/src/views/virtualization/HostDetail.vue @@ -131,6 +131,20 @@ +
+

+ 硬盘 IO 限制 + +

+
+
+
+ {{ f.label }} + {{ formatDiskIoVal(detail[f.key], f) }} +
+
+
+
@@ -408,6 +422,19 @@ Mbps +
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
其他配置
@@ -475,6 +502,19 @@
+
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
令牌有效期
@@ -536,7 +576,7 @@ import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Connection, Search, Plus, Key, CopyDocument } from '@element-plus/icons-vue' +import { ArrowLeft, ArrowRight, Refresh, Edit, Delete, Monitor, Coin, Connection, Search, Plus, Key, CopyDocument } from '@element-plus/icons-vue' import { getRemoteHostDetail, updateRemoteHost, deleteRemoteHost, getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking, @@ -630,9 +670,39 @@ const editDialogVisible = ref(false) const showGroupSelector = ref(false) const formRef = ref(null) +const diskIoDefaults = { + read_bytes_sec: 314572800, write_bytes_sec: 314572800, + read_iops_sec: 1000, write_iops_sec: 1000, + read_bytes_sec_max: 314572800, write_bytes_sec_max: 314572800, + read_iops_sec_max: 1000, write_iops_sec_max: 1000 +} +const diskIoFields = [ + { key: 'read_bytes_sec', label: '读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec', label: '写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec', label: '读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec', label: '写入 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'read_bytes_sec_max', label: '突发读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec_max', label: '突发写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec_max', label: '突发读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec_max', label: '突发写入 IOPS', unit: 'IOPS', isBandwidth: false } +] +const formatDiskIoVal = (val, field) => { + if (!val && val !== 0) return '-' + val = Number(val) + if (!field.isBandwidth) return val.toLocaleString() + ' ' + field.unit + if (val >= 1073741824) return (val / 1073741824).toFixed(1) + ' GB/s' + if (val >= 1048576) return (val / 1048576).toFixed(0) + ' MB/s' + if (val >= 1024) return (val / 1024).toFixed(0) + ' KB/s' + return val + ' B/s' +} +const showDiskIoSection = ref(false) +const showTokenDiskIo = ref(false) +const showDetailDiskIo = ref(false) + const formData = reactive({ name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', - max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '' + max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '', + ...diskIoDefaults }) const formRules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }], @@ -899,8 +969,10 @@ const handleEdit = () => { port: d.port || 22, user: d.user || '', password: d.password || '', private_key: d.private_key || '', max_cpu: d.max_cpu || 0, max_memory: d.max_memory || 0, max_disk: d.max_disk || 0, rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0, - host_group_id: d.host_group_id || 0, description: d.description || '' + host_group_id: d.host_group_id || 0, description: d.description || '', + ...Object.fromEntries(diskIoFields.map(f => [f.key, d[f.key] ?? diskIoDefaults[f.key]])) }) + showDiskIoSection.value = diskIoFields.some(f => d[f.key] && d[f.key] !== diskIoDefaults[f.key]) editDialogVisible.value = true } @@ -947,7 +1019,8 @@ const tokenForm = reactive({ name: '', host_group_id: 0, max_cpu: 4, max_memory: 4194304, max_disk: 100, rx_bandwidth: 100, tx_bandwidth: 100, - description: '', expire_hours: 24 + description: '', expire_hours: 24, + ...diskIoDefaults }) const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 }) const tokenRules = { @@ -979,10 +1052,12 @@ const openTokenDialog = () => { max_disk: d?.max_disk || 100, rx_bandwidth: d?.rx_bandwidth || 100, tx_bandwidth: d?.tx_bandwidth || 100, - description: '', expire_hours: 24 + description: '', expire_hours: 24, + ...Object.fromEntries(diskIoFields.map(f => [f.key, d?.[f.key] ?? diskIoDefaults[f.key]])) }) tokenMemUnit.value = 'GB' tokenDiskUnit.value = 'GB' + showTokenDiskIo.value = false tokenDialogVisible.value = true } @@ -1004,6 +1079,7 @@ const handleTokenSubmit = () => { fd.append('max_disk', tokenForm.max_disk) fd.append('rx_bandwidth', tokenForm.rx_bandwidth) fd.append('tx_bandwidth', tokenForm.tx_bandwidth) + diskIoFields.forEach(f => { if (tokenForm[f.key] !== undefined) fd.append(f.key, tokenForm[f.key]) }) fd.append('description', tokenForm.description || '') fd.append('expire_hours', tokenForm.expire_hours) const res = await createHostToken(fd) @@ -1294,4 +1370,12 @@ onBeforeUnmount(() => { isPageActive = false; disposeCharts() }) .metric-summary-value { font-size: 22px; font-weight: 600; color: #1d2129; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .metric-summary-sub { font-size: 12px; color: #86909c; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; } +.clickable:hover { color: #409eff; } +.section-arrow { transition: transform 0.2s; font-size: 14px; } +.section-arrow.expanded { transform: rotate(90deg); } +.section-hint { font-size: 12px; color: #909399; font-weight: 400; } +.tk-section-title.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; } +.tk-section-title.clickable:hover { color: #409eff; } + diff --git a/src/views/virtualization/HostManage.vue b/src/views/virtualization/HostManage.vue index 36aa164..f2f4c37 100644 --- a/src/views/virtualization/HostManage.vue +++ b/src/views/virtualization/HostManage.vue @@ -159,6 +159,19 @@
+
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
其他配置
@@ -271,6 +284,19 @@
+
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
令牌有效期
@@ -378,7 +404,7 @@ import { ref, reactive, computed, inject, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Refresh, Search, ArrowLeft, Monitor, Coin, Connection, Key, CopyDocument } from '@element-plus/icons-vue' +import { Plus, Refresh, Search, ArrowLeft, ArrowRight, Monitor, Coin, Connection, Key, CopyDocument } from '@element-plus/icons-vue' import { getRemoteHostList, getRemoteHostDetail, addRemoteHost, updateRemoteHost, deleteRemoteHost, @@ -418,12 +444,42 @@ const currentDetail = ref(null) const metricsVisible = ref(false) const metricsData = ref(null) +const diskIoDefaults = { + read_bytes_sec: 314572800, write_bytes_sec: 314572800, + read_iops_sec: 1000, write_iops_sec: 1000, + read_bytes_sec_max: 314572800, write_bytes_sec_max: 314572800, + read_iops_sec_max: 1000, write_iops_sec_max: 1000 +} +const diskIoFields = [ + { key: 'read_bytes_sec', label: '读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec', label: '写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec', label: '读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec', label: '写入 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'read_bytes_sec_max', label: '突发读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec_max', label: '突发写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec_max', label: '突发读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec_max', label: '突发写入 IOPS', unit: 'IOPS', isBandwidth: false } +] +const formatDiskIoVal = (val, field) => { + if (!val && val !== 0) return '-' + val = Number(val) + if (!field.isBandwidth) return val.toLocaleString() + ' ' + field.unit + if (val >= 1073741824) return (val / 1073741824).toFixed(1) + ' GB/s' + if (val >= 1048576) return (val / 1048576).toFixed(0) + ' MB/s' + if (val >= 1024) return (val / 1024).toFixed(0) + ' KB/s' + return val + ' B/s' +} + +const showDiskIoSection = ref(false) +const showTokenDiskIo = ref(false) + const formData = reactive({ id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '', - _groupName: '' + _groupName: '', + ...diskIoDefaults }) const formRules = { @@ -522,8 +578,9 @@ const resetForm = () => { port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '', - _groupName: '' + _groupName: '', ...diskIoDefaults }) + showDiskIoSection.value = false } const handleHostGroupSelected = (group) => { @@ -585,9 +642,10 @@ const handleEdit = (row) => { max_cpu: row.max_cpu || 0, max_memory: row.max_memory || 0, max_disk: row.max_disk || 0, rx_bandwidth: row.rx_bandwidth || 0, tx_bandwidth: row.tx_bandwidth || 0, host_group_id: row.host_group_id || 0, description: row.description || '', - _groupName: getGroupName(row.host_group_id) + _groupName: getGroupName(row.host_group_id), + ...Object.fromEntries(diskIoFields.map(f => [f.key, row[f.key] ?? diskIoDefaults[f.key]])) }) - // 异步获取详情以补全password等字段 + showDiskIoSection.value = diskIoFields.some(f => row[f.key] && row[f.key] !== diskIoDefaults[f.key]) getRemoteHostDetail({ service_id: serviceId.value, id: row.id }).then(res => { const body = res?.data if (body?.code === 200 && body?.data) { @@ -595,6 +653,7 @@ const handleEdit = (row) => { if (detail.password) formData.password = detail.password if (detail.token) formData.token = detail.token if (detail.private_key) formData.private_key = detail.private_key + diskIoFields.forEach(f => { if (detail[f.key] !== undefined) formData[f.key] = detail[f.key] }) } }).catch(() => {}) dialogVisible.value = true @@ -719,7 +778,8 @@ const tokenForm = reactive({ max_memory: 4194304, max_disk: 100, rx_bandwidth: 100, tx_bandwidth: 100, description: '', expire_hours: 24, - _groupName: '' + _groupName: '', + ...diskIoDefaults }) const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 }) @@ -750,10 +810,12 @@ const openTokenDialog = () => { name: '', host_group_id: 0, max_cpu: 4, max_memory: 4194304, max_disk: 100, rx_bandwidth: 100, tx_bandwidth: 100, - description: '', expire_hours: 24, _groupName: '' + description: '', expire_hours: 24, _groupName: '', + ...diskIoDefaults }) tokenMemUnit.value = 'GB' tokenDiskUnit.value = 'GB' + showTokenDiskIo.value = false tokenDialogVisible.value = true } @@ -776,6 +838,7 @@ const handleTokenSubmit = () => { fd.append('max_disk', tokenForm.max_disk) fd.append('rx_bandwidth', tokenForm.rx_bandwidth) fd.append('tx_bandwidth', tokenForm.tx_bandwidth) + diskIoFields.forEach(f => { if (tokenForm[f.key] !== undefined) fd.append(f.key, tokenForm[f.key]) }) fd.append('description', tokenForm.description || '') fd.append('expire_hours', tokenForm.expire_hours) @@ -832,4 +895,9 @@ onMounted(() => { .metrics-card { margin-bottom: 12px; } .metrics-title { font-weight: 600; font-size: 14px; display: inline-flex; align-items: center; gap: 6px; } .metrics-title .el-icon { font-size: 16px; color: #409eff; } +.tk-section-title.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; } +.tk-section-title.clickable:hover { color: #409eff; } +.section-arrow { transition: transform 0.2s; font-size: 14px; } +.section-arrow.expanded { transform: rotate(90deg); } +.section-hint { font-size: 12px; color: #909399; font-weight: 400; } diff --git a/src/views/virtualization/HostTreeManage.vue b/src/views/virtualization/HostTreeManage.vue index f5d20e0..4b1584a 100644 --- a/src/views/virtualization/HostTreeManage.vue +++ b/src/views/virtualization/HostTreeManage.vue @@ -131,6 +131,19 @@
+
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
令牌有效期
@@ -326,6 +339,19 @@
+
+
+ 硬盘 IO 限制 + + 可选,不展开则使用默认值 +
+
+ + + {{ f.unit }} + +
+
其他配置
@@ -625,6 +651,25 @@ const handleOptimalHost = async (row) => { } // ---- 宿主机 CRUD ---- +const diskIoDefaults = { + read_bytes_sec: 314572800, write_bytes_sec: 314572800, + read_iops_sec: 1000, write_iops_sec: 1000, + read_bytes_sec_max: 314572800, write_bytes_sec_max: 314572800, + read_iops_sec_max: 1000, write_iops_sec_max: 1000 +} +const diskIoFields = [ + { key: 'read_bytes_sec', label: '读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec', label: '写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec', label: '读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec', label: '写入 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'read_bytes_sec_max', label: '突发读取带宽', unit: 'B/s', isBandwidth: true }, + { key: 'write_bytes_sec_max', label: '突发写入带宽', unit: 'B/s', isBandwidth: true }, + { key: 'read_iops_sec_max', label: '突发读取 IOPS', unit: 'IOPS', isBandwidth: false }, + { key: 'write_iops_sec_max', label: '突发写入 IOPS', unit: 'IOPS', isBandwidth: false } +] +const showHostDiskIo = ref(false) +const showTokenDiskIo = ref(false) + const hostDialogVisible = ref(false) const hostDialogType = ref('add') const hostFormRef = ref(null) @@ -632,7 +677,8 @@ const hostForm = reactive({ id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, - rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '' + rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '', + ...diskIoDefaults }) const hostFormRules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }], @@ -642,13 +688,15 @@ const hostFormRules = { const handleAddHost = () => { hostDialogType.value = 'add' - Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '' }) + Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '', ...diskIoDefaults }) + showHostDiskIo.value = false hostDialogVisible.value = true } const handleAddHostToGroup = (group) => { hostDialogType.value = 'add' - Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: group.id, description: '' }) + Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: group.id, description: '', ...diskIoDefaults }) + showHostDiskIo.value = false hostDialogVisible.value = true } @@ -659,14 +707,17 @@ const handleEditHost = (row) => { port: row.port || 22, user: row.user || '', password: row.password || '', private_key: row.private_key || '', max_cpu: row.max_cpu || 0, max_memory: row.max_memory || 0, max_disk: row.max_disk || 0, rx_bandwidth: row.rx_bandwidth || 0, tx_bandwidth: row.tx_bandwidth || 0, - host_group_id: row.host_group_id || 0, description: row.description || '' + host_group_id: row.host_group_id || 0, description: row.description || '', + ...Object.fromEntries(diskIoFields.map(f => [f.key, row[f.key] ?? diskIoDefaults[f.key]])) }) + showHostDiskIo.value = diskIoFields.some(f => row[f.key] && row[f.key] !== diskIoDefaults[f.key]) getRemoteHostDetail({ service_id: serviceId.value, id: row.id }).then(res => { if (res?.data?.code === 200 && res?.data?.data) { const d = res.data.data.host ?? res.data.data.data ?? res.data.data if (d.password) hostForm.password = d.password if (d.token) hostForm.token = d.token if (d.private_key) hostForm.private_key = d.private_key + diskIoFields.forEach(f => { if (d[f.key] !== undefined) hostForm[f.key] = d[f.key] }) } }).catch(() => {}) hostDialogVisible.value = true @@ -723,7 +774,8 @@ const tokenForm = reactive({ name: '', host_group_id: 0, max_cpu: 4, max_memory: 4194304, max_disk: 100, rx_bandwidth: 100, tx_bandwidth: 100, - description: '', expire_hours: 24 + description: '', expire_hours: 24, + ...diskIoDefaults }) const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 }) const tokenRules = { @@ -751,10 +803,12 @@ const openTokenDialog = () => { name: '', host_group_id: 0, max_cpu: 4, max_memory: 4194304, max_disk: 100, rx_bandwidth: 100, tx_bandwidth: 100, - description: '', expire_hours: 24 + description: '', expire_hours: 24, + ...diskIoDefaults }) tokenMemUnit.value = 'GB' tokenDiskUnit.value = 'GB' + showTokenDiskIo.value = false tokenDialogVisible.value = true } @@ -772,6 +826,7 @@ const handleTokenSubmit = () => { fd.append('max_disk', tokenForm.max_disk) fd.append('rx_bandwidth', tokenForm.rx_bandwidth) fd.append('tx_bandwidth', tokenForm.tx_bandwidth) + diskIoFields.forEach(f => { if (tokenForm[f.key] !== undefined) fd.append(f.key, tokenForm[f.key]) }) fd.append('description', tokenForm.description || '') fd.append('expire_hours', tokenForm.expire_hours) const res = await createHostToken(fd) @@ -839,4 +894,9 @@ onMounted(() => { if (serviceId.value) loadTreeData() }) .host-addr { color: #409eff; font-size: 13px; } .host-url { color: #909399; font-size: 12px; } +.tk-section-title.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; } +.tk-section-title.clickable:hover { color: #409eff; } +.section-arrow { transition: transform 0.2s; font-size: 14px; } +.section-arrow.expanded { transform: rotate(90deg); } +.section-hint { font-size: 12px; color: #909399; font-weight: 400; }