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:
shiran
2026-05-14 13:00:26 +08:00
parent 59c5d16082
commit dc63020943
3 changed files with 230 additions and 18 deletions
+89 -5
View File
@@ -131,6 +131,20 @@
</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 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>
</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-title">其他配置</div>
<el-form-item label="宿主机组">
@@ -475,6 +502,19 @@
</el-form-item>
</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-title">令牌有效期</div>
<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 { 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; }
</style>
+75 -7
View File
@@ -159,6 +159,19 @@
</el-form-item>
</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-title">其他配置</div>
<el-form-item label="宿主机组">
@@ -271,6 +284,19 @@
</el-form-item>
</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-title">令牌有效期</div>
<el-form-item label="有效期" prop="expire_hours">
@@ -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; }
</style>
+66 -6
View File
@@ -131,6 +131,19 @@
</el-form-item>
</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-title">令牌有效期</div>
<el-form-item label="有效期" prop="expire_hours">
@@ -326,6 +339,19 @@
</el-form-item>
</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-title">其他配置</div>
<el-form-item label="宿主机组">
@@ -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; }
</style>