feat(admin/host): 宿主机表单与详情增加硬盘IO限制8字段(可折叠动态展示) -- 缘由: 后端新增 read/write_bytes_sec, read/write_iops_sec 及突发对应字段 -- 预期: HostManage/HostDetail/HostTreeManage 的新增/编辑/令牌表单含可折叠IO参数区, 详情页可展开查看IO限制值
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -131,6 +131,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section-block">
|
||||||
|
<h3 class="section-title clickable" @click="showDetailDiskIo = !showDetailDiskIo">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showDetailDiskIo }"><ArrowRight /></el-icon>
|
||||||
|
</h3>
|
||||||
|
<div v-show="showDetailDiskIo" class="config-grid">
|
||||||
|
<div class="config-row" v-for="i in Math.ceil(diskIoFields.length / 3)" :key="i">
|
||||||
|
<div class="config-cell" v-for="f in diskIoFields.slice((i - 1) * 3, i * 3)" :key="f.key">
|
||||||
|
<span class="config-label">{{ f.label }}</span>
|
||||||
|
<span class="config-value mono-text">{{ formatDiskIoVal(detail[f.key], f) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="监控" name="monitor">
|
<el-tab-pane label="监控" name="monitor">
|
||||||
@@ -408,6 +422,19 @@
|
|||||||
<el-form-item label="上行带宽"><el-input-number v-model="formData.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span></el-form-item>
|
<el-form-item label="上行带宽"><el-input-number v-model="formData.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showDiskIoSection = !showDiskIoSection">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showDiskIoSection }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showDiskIoSection" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label">
|
||||||
|
<el-input-number v-model="formData[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">其他配置</div>
|
<div class="tk-section-title">其他配置</div>
|
||||||
<el-form-item label="宿主机组">
|
<el-form-item label="宿主机组">
|
||||||
@@ -475,6 +502,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showTokenDiskIo = !showTokenDiskIo">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showTokenDiskIo }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showTokenDiskIo" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label" class="tk-res-item">
|
||||||
|
<el-input-number v-model="tokenForm[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">令牌有效期</div>
|
<div class="tk-section-title">令牌有效期</div>
|
||||||
<el-form-item label="有效期" prop="expire_hours">
|
<el-form-item label="有效期" prop="expire_hours">
|
||||||
@@ -536,7 +576,7 @@
|
|||||||
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
|
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 {
|
import {
|
||||||
getRemoteHostDetail, updateRemoteHost, deleteRemoteHost,
|
getRemoteHostDetail, updateRemoteHost, deleteRemoteHost,
|
||||||
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
|
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
|
||||||
@@ -630,9 +670,39 @@ const editDialogVisible = ref(false)
|
|||||||
const showGroupSelector = ref(false)
|
const showGroupSelector = ref(false)
|
||||||
const formRef = ref(null)
|
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({
|
const formData = reactive({
|
||||||
name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '',
|
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 = {
|
const formRules = {
|
||||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
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 || '',
|
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,
|
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,
|
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
|
editDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,7 +1019,8 @@ const tokenForm = reactive({
|
|||||||
name: '', host_group_id: 0, max_cpu: 4,
|
name: '', host_group_id: 0, max_cpu: 4,
|
||||||
max_memory: 4194304, max_disk: 100,
|
max_memory: 4194304, max_disk: 100,
|
||||||
rx_bandwidth: 100, tx_bandwidth: 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 tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 })
|
||||||
const tokenRules = {
|
const tokenRules = {
|
||||||
@@ -979,10 +1052,12 @@ const openTokenDialog = () => {
|
|||||||
max_disk: d?.max_disk || 100,
|
max_disk: d?.max_disk || 100,
|
||||||
rx_bandwidth: d?.rx_bandwidth || 100,
|
rx_bandwidth: d?.rx_bandwidth || 100,
|
||||||
tx_bandwidth: d?.tx_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'
|
tokenMemUnit.value = 'GB'
|
||||||
tokenDiskUnit.value = 'GB'
|
tokenDiskUnit.value = 'GB'
|
||||||
|
showTokenDiskIo.value = false
|
||||||
tokenDialogVisible.value = true
|
tokenDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,6 +1079,7 @@ const handleTokenSubmit = () => {
|
|||||||
fd.append('max_disk', tokenForm.max_disk)
|
fd.append('max_disk', tokenForm.max_disk)
|
||||||
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
||||||
fd.append('tx_bandwidth', tokenForm.tx_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('description', tokenForm.description || '')
|
||||||
fd.append('expire_hours', tokenForm.expire_hours)
|
fd.append('expire_hours', tokenForm.expire_hours)
|
||||||
const res = await createHostToken(fd)
|
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-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; }
|
.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; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -159,6 +159,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showDiskIoSection = !showDiskIoSection">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showDiskIoSection }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showDiskIoSection" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label">
|
||||||
|
<el-input-number v-model="formData[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">其他配置</div>
|
<div class="tk-section-title">其他配置</div>
|
||||||
<el-form-item label="宿主机组">
|
<el-form-item label="宿主机组">
|
||||||
@@ -271,6 +284,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showTokenDiskIo = !showTokenDiskIo">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showTokenDiskIo }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showTokenDiskIo" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label" class="tk-res-item">
|
||||||
|
<el-input-number v-model="tokenForm[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">令牌有效期</div>
|
<div class="tk-section-title">令牌有效期</div>
|
||||||
<el-form-item label="有效期" prop="expire_hours">
|
<el-form-item label="有效期" prop="expire_hours">
|
||||||
@@ -378,7 +404,7 @@
|
|||||||
import { ref, reactive, computed, inject, onMounted } from 'vue'
|
import { ref, reactive, computed, inject, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 {
|
import {
|
||||||
getRemoteHostList, getRemoteHostDetail,
|
getRemoteHostList, getRemoteHostDetail,
|
||||||
addRemoteHost, updateRemoteHost, deleteRemoteHost,
|
addRemoteHost, updateRemoteHost, deleteRemoteHost,
|
||||||
@@ -418,12 +444,42 @@ const currentDetail = ref(null)
|
|||||||
const metricsVisible = ref(false)
|
const metricsVisible = ref(false)
|
||||||
const metricsData = ref(null)
|
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({
|
const formData = reactive({
|
||||||
id: undefined, name: '', base_url: '', ip: '', token: '',
|
id: undefined, name: '', base_url: '', ip: '', token: '',
|
||||||
port: 22, user: '', password: '', private_key: '',
|
port: 22, user: '', password: '', private_key: '',
|
||||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
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: '',
|
||||||
_groupName: ''
|
_groupName: '',
|
||||||
|
...diskIoDefaults
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
@@ -522,8 +578,9 @@ const resetForm = () => {
|
|||||||
port: 22, user: '', password: '', private_key: '',
|
port: 22, user: '', password: '', private_key: '',
|
||||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
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: '',
|
||||||
_groupName: ''
|
_groupName: '', ...diskIoDefaults
|
||||||
})
|
})
|
||||||
|
showDiskIoSection.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHostGroupSelected = (group) => {
|
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,
|
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,
|
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 || '',
|
||||||
_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 => {
|
getRemoteHostDetail({ service_id: serviceId.value, id: row.id }).then(res => {
|
||||||
const body = res?.data
|
const body = res?.data
|
||||||
if (body?.code === 200 && body?.data) {
|
if (body?.code === 200 && body?.data) {
|
||||||
@@ -595,6 +653,7 @@ const handleEdit = (row) => {
|
|||||||
if (detail.password) formData.password = detail.password
|
if (detail.password) formData.password = detail.password
|
||||||
if (detail.token) formData.token = detail.token
|
if (detail.token) formData.token = detail.token
|
||||||
if (detail.private_key) formData.private_key = detail.private_key
|
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(() => {})
|
}).catch(() => {})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@@ -719,7 +778,8 @@ const tokenForm = reactive({
|
|||||||
max_memory: 4194304, max_disk: 100,
|
max_memory: 4194304, max_disk: 100,
|
||||||
rx_bandwidth: 100, tx_bandwidth: 100,
|
rx_bandwidth: 100, tx_bandwidth: 100,
|
||||||
description: '', expire_hours: 24,
|
description: '', expire_hours: 24,
|
||||||
_groupName: ''
|
_groupName: '',
|
||||||
|
...diskIoDefaults
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 })
|
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,
|
name: '', host_group_id: 0, max_cpu: 4,
|
||||||
max_memory: 4194304, max_disk: 100,
|
max_memory: 4194304, max_disk: 100,
|
||||||
rx_bandwidth: 100, tx_bandwidth: 100,
|
rx_bandwidth: 100, tx_bandwidth: 100,
|
||||||
description: '', expire_hours: 24, _groupName: ''
|
description: '', expire_hours: 24, _groupName: '',
|
||||||
|
...diskIoDefaults
|
||||||
})
|
})
|
||||||
tokenMemUnit.value = 'GB'
|
tokenMemUnit.value = 'GB'
|
||||||
tokenDiskUnit.value = 'GB'
|
tokenDiskUnit.value = 'GB'
|
||||||
|
showTokenDiskIo.value = false
|
||||||
tokenDialogVisible.value = true
|
tokenDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,6 +838,7 @@ const handleTokenSubmit = () => {
|
|||||||
fd.append('max_disk', tokenForm.max_disk)
|
fd.append('max_disk', tokenForm.max_disk)
|
||||||
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
||||||
fd.append('tx_bandwidth', tokenForm.tx_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('description', tokenForm.description || '')
|
||||||
fd.append('expire_hours', tokenForm.expire_hours)
|
fd.append('expire_hours', tokenForm.expire_hours)
|
||||||
|
|
||||||
@@ -832,4 +895,9 @@ onMounted(() => {
|
|||||||
.metrics-card { margin-bottom: 12px; }
|
.metrics-card { margin-bottom: 12px; }
|
||||||
.metrics-title { font-weight: 600; font-size: 14px; display: inline-flex; align-items: center; gap: 6px; }
|
.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; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -131,6 +131,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showTokenDiskIo = !showTokenDiskIo">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showTokenDiskIo }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showTokenDiskIo" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label" class="tk-res-item">
|
||||||
|
<el-input-number v-model="tokenForm[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">令牌有效期</div>
|
<div class="tk-section-title">令牌有效期</div>
|
||||||
<el-form-item label="有效期" prop="expire_hours">
|
<el-form-item label="有效期" prop="expire_hours">
|
||||||
@@ -326,6 +339,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tk-section">
|
||||||
|
<div class="tk-section-title clickable" @click="showHostDiskIo = !showHostDiskIo">
|
||||||
|
硬盘 IO 限制
|
||||||
|
<el-icon class="section-arrow" :class="{ expanded: showHostDiskIo }"><ArrowRight /></el-icon>
|
||||||
|
<span class="section-hint">可选,不展开则使用默认值</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showHostDiskIo" class="tk-resource-grid">
|
||||||
|
<el-form-item v-for="f in diskIoFields" :key="f.key" :label="f.label">
|
||||||
|
<el-input-number v-model="hostForm[f.key]" :min="0" controls-position="right" />
|
||||||
|
<span class="tk-res-unit">{{ f.unit }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tk-section">
|
<div class="tk-section">
|
||||||
<div class="tk-section-title">其他配置</div>
|
<div class="tk-section-title">其他配置</div>
|
||||||
<el-form-item label="宿主机组">
|
<el-form-item label="宿主机组">
|
||||||
@@ -625,6 +651,25 @@ const handleOptimalHost = async (row) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 宿主机 CRUD ----
|
// ---- 宿主机 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 hostDialogVisible = ref(false)
|
||||||
const hostDialogType = ref('add')
|
const hostDialogType = ref('add')
|
||||||
const hostFormRef = ref(null)
|
const hostFormRef = ref(null)
|
||||||
@@ -632,7 +677,8 @@ const hostForm = reactive({
|
|||||||
id: undefined, name: '', base_url: '', ip: '', token: '',
|
id: undefined, name: '', base_url: '', ip: '', token: '',
|
||||||
port: 22, user: '', password: '', private_key: '',
|
port: 22, user: '', password: '', private_key: '',
|
||||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
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 = {
|
const hostFormRules = {
|
||||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
@@ -642,13 +688,15 @@ const hostFormRules = {
|
|||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
hostDialogType.value = 'add'
|
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
|
hostDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddHostToGroup = (group) => {
|
const handleAddHostToGroup = (group) => {
|
||||||
hostDialogType.value = 'add'
|
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
|
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 || '',
|
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,
|
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,
|
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 => {
|
getRemoteHostDetail({ service_id: serviceId.value, id: row.id }).then(res => {
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
const d = res.data.data.host ?? res.data.data.data ?? 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.password) hostForm.password = d.password
|
||||||
if (d.token) hostForm.token = d.token
|
if (d.token) hostForm.token = d.token
|
||||||
if (d.private_key) hostForm.private_key = d.private_key
|
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(() => {})
|
}).catch(() => {})
|
||||||
hostDialogVisible.value = true
|
hostDialogVisible.value = true
|
||||||
@@ -723,7 +774,8 @@ const tokenForm = reactive({
|
|||||||
name: '', host_group_id: 0, max_cpu: 4,
|
name: '', host_group_id: 0, max_cpu: 4,
|
||||||
max_memory: 4194304, max_disk: 100,
|
max_memory: 4194304, max_disk: 100,
|
||||||
rx_bandwidth: 100, tx_bandwidth: 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 tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 })
|
||||||
const tokenRules = {
|
const tokenRules = {
|
||||||
@@ -751,10 +803,12 @@ const openTokenDialog = () => {
|
|||||||
name: '', host_group_id: 0, max_cpu: 4,
|
name: '', host_group_id: 0, max_cpu: 4,
|
||||||
max_memory: 4194304, max_disk: 100,
|
max_memory: 4194304, max_disk: 100,
|
||||||
rx_bandwidth: 100, tx_bandwidth: 100,
|
rx_bandwidth: 100, tx_bandwidth: 100,
|
||||||
description: '', expire_hours: 24
|
description: '', expire_hours: 24,
|
||||||
|
...diskIoDefaults
|
||||||
})
|
})
|
||||||
tokenMemUnit.value = 'GB'
|
tokenMemUnit.value = 'GB'
|
||||||
tokenDiskUnit.value = 'GB'
|
tokenDiskUnit.value = 'GB'
|
||||||
|
showTokenDiskIo.value = false
|
||||||
tokenDialogVisible.value = true
|
tokenDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,6 +826,7 @@ const handleTokenSubmit = () => {
|
|||||||
fd.append('max_disk', tokenForm.max_disk)
|
fd.append('max_disk', tokenForm.max_disk)
|
||||||
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
||||||
fd.append('tx_bandwidth', tokenForm.tx_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('description', tokenForm.description || '')
|
||||||
fd.append('expire_hours', tokenForm.expire_hours)
|
fd.append('expire_hours', tokenForm.expire_hours)
|
||||||
const res = await createHostToken(fd)
|
const res = await createHostToken(fd)
|
||||||
@@ -839,4 +894,9 @@ onMounted(() => { if (serviceId.value) loadTreeData() })
|
|||||||
|
|
||||||
.host-addr { color: #409eff; font-size: 13px; }
|
.host-addr { color: #409eff; font-size: 13px; }
|
||||||
.host-url { color: #909399; font-size: 12px; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user