feat(admin/user-vm): 流量上限展示加修改入口、网络tab加删除网络操作
缘由: 1) 虚拟机详情页(UserVmDetail.vue / VmDetail.vue)中"流量上限"原仅展示无修改入口,对应 docs/2026.05.08.12.37-update.json 中 update_traffic 接口已支持 traffic_max + traffic_exhausted_rx/tx_mbps 修改,但用户需从"更多 dropdown"绕一道才能到达。 2) /user-goods/vm-detail 网络管理 tab 缺少"删除网络"操作。 修改: - UserVmDetail.vue:流量上限单元格内追加"修改"小按钮,复用既有 updateTraffic 弹窗(已覆盖 update_traffic 全部新字段,不动接口逻辑);网络表格新增"操作"列+删除按钮,调用 host_service/point/network/delete;row 上若缺 service_id/host_id 用 getUserVmNetworkDetail 反查兜底,仍取不到则提示并阻止;二次确认弹窗明示该操作会影响所有绑定该网络的虚拟机。 - VmDetail.vue:流量上限单元格内追加"修改"小按钮,复用 handleUpdateTraffic(host_service/point/vm/update_traffic)。 预期: - 详情页用户在"流量上限"位置可一键进入修改弹窗,无需走 dropdown。 - vm-detail 网络tab 表格每行可触发"删除网络"流程,含强提示与兜底取值。 - 不引入新依赖;trafficVisible 弹窗保持向 docs 字段对齐;UI 微调仅限新增样式 .cfg-edit-btn 与一列操作列。 未测试:未在 admin_dashboard_pc 本地 dev 验证(终端仅运行 user_dashboard_pc),需联调 update_traffic 与 point/network/delete 实际返回。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -129,7 +129,15 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-cell"><span class="config-label">流量上限</span><span class="config-value">{{ formatTraffic(vm.traffic_max) }}</span></div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">流量上限</span>
|
||||
<span class="config-value">
|
||||
{{ formatTraffic(vm.traffic_max) }}
|
||||
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleCommand('updateTraffic')">
|
||||
<el-icon :size="14"><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-cell"><span class="config-label">续费价格</span><span class="config-value">¥{{ (userGoods.renewPrice / 100).toFixed(2) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">基础价格</span><span class="config-value">¥{{ (userGoods.basePrice / 100).toFixed(2) }}</span></div>
|
||||
</div>
|
||||
@@ -306,6 +314,13 @@
|
||||
<el-table-column prop="gateway" label="网关" min-width="120" />
|
||||
<el-table-column prop="mac_address" label="MAC" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80"><template #default="{ row }"><el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.type === 'bridge' ? '网桥' : 'NAT' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" size="small" :loading="deletingNetworkId === row.id" @click="handleDeleteVmNetwork(row)">
|
||||
<el-icon :size="14"><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!vmNetworks.length" :image-size="60" description="暂无网络" />
|
||||
</el-tab-pane>
|
||||
@@ -1072,7 +1087,7 @@
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled, View, Hide, CopyDocument } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled, View, Hide, CopyDocument, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserVmDetail, getUserVmVnc, getUserVmHostImages,
|
||||
startUserVm, stopUserVm, rebootUserVm, suspendUserVm, resumeUserVm, rescueUserVm, exitRescueUserVm, rebuildUserVm, deleteUserVm,
|
||||
@@ -1083,11 +1098,12 @@ import {
|
||||
getUserVmPostGroupList, createUserVmPostGroup, updateUserVmPostGroup, bindUserVmPostGroup, unbindUserVmPostGroup, applyUserVmPostGroup, deleteUserVmPostGroup, enableUserVmPostGroupWhitelist, disableUserVmPostGroupWhitelist,
|
||||
createUserVmPostGroupRule, updateUserVmPostGroupRule, deleteUserVmPostGroupRule,
|
||||
getUserVmPostGroupDetail,
|
||||
getUserVmNetworkList, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
|
||||
getUserVmNetworkList, getUserVmNetworkDetail, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
|
||||
getUserGoodsDetail,
|
||||
getUserVmMetricsHistory,
|
||||
getUserVmTrafficPolicy, updateUserVmTrafficPolicy, addUserVmFixedTraffic, addUserVmTemporaryTraffic
|
||||
} from '@/api/admin/userVm'
|
||||
import { deleteNetwork as deletePointNetwork } from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
@@ -1644,6 +1660,49 @@ const loadNetworks = async () => {
|
||||
} catch { /* */ } finally { networkLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- 删除网络 ----
|
||||
// 走 host_service/point/network/delete:删除底层物理网络(破坏性,会影响其他绑定该网络的 VM)
|
||||
// row 字段可能不完整,service_id / host_id 通过 getUserVmNetworkDetail 兜底反查
|
||||
const deletingNetworkId = ref(0)
|
||||
const resolveNetworkServiceHost = async (row) => {
|
||||
let serviceId = row.service_id ?? row.host_service_id ?? row.kvm_service_id ?? 0
|
||||
let hostId = row.host_id ?? 0
|
||||
if (!serviceId || !hostId) {
|
||||
try {
|
||||
const res = await getUserVmNetworkDetail({ user_goods_id: userGoodsId.value, id: row.id })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
const n = d.data || d.network || d
|
||||
if (!serviceId) serviceId = n?.service_id ?? n?.host_service_id ?? n?.kvm_service_id ?? 0
|
||||
if (!hostId) hostId = n?.host_id ?? 0
|
||||
}
|
||||
} catch { /* 兜底失败时返回原值,由调用方判断 */ }
|
||||
}
|
||||
return { serviceId, hostId }
|
||||
}
|
||||
const handleDeleteVmNetwork = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`将删除底层网络「${row.name}」(ID:${row.id}),该操作会影响所有绑定该网络的虚拟机,是否继续?`,
|
||||
'删除网络',
|
||||
{ confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' }
|
||||
).then(async () => {
|
||||
deletingNetworkId.value = row.id
|
||||
try {
|
||||
const { serviceId, hostId } = await resolveNetworkServiceHost(row)
|
||||
if (!serviceId) { ElMessage.error('无法获取该网络所属服务ID,删除失败'); return }
|
||||
const params = { service_id: serviceId, network_id: row.id }
|
||||
if (hostId) params.host_id = hostId
|
||||
const res = await deletePointNetwork(params)
|
||||
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '删除失败'))
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '删除失败'))
|
||||
} finally {
|
||||
deletingNetworkId.value = 0
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// ---- 绑定网络 ----
|
||||
const showBindNetworkSelector = ref(false)
|
||||
|
||||
@@ -2355,6 +2414,8 @@ onBeforeUnmount(() => { disposeCharts() })
|
||||
.config-cell:last-child { border-right: none; }
|
||||
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
|
||||
.config-value { font-size: 13px; color: #1d2129; line-height: 1.4; word-break: break-all; }
|
||||
.cfg-edit-btn { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
|
||||
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
|
||||
.pwd-value { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.pwd-text { font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; background: #f5f7fa; padding: 2px 8px; border-radius: 3px; letter-spacing: .5px; user-select: all; }
|
||||
.pwd-btn { padding: 0 !important; height: auto !important; min-height: auto !important; }
|
||||
|
||||
@@ -222,7 +222,12 @@
|
||||
<div class="config-row">
|
||||
<div class="config-cell">
|
||||
<span class="config-label">流量上限</span>
|
||||
<span class="config-value">{{ formatTrafficMax(detail.traffic_max) }}</span>
|
||||
<span class="config-value">
|
||||
{{ formatTrafficMax(detail.traffic_max) }}
|
||||
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleUpdateTraffic" :disabled="isMigrating">
|
||||
<el-icon :size="14"><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">快照配额</span>
|
||||
@@ -1564,7 +1569,7 @@
|
||||
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Plus, Search, WarningFilled, Loading } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Plus, Search, WarningFilled, Loading, Edit } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getVmDetail, getVmStatus,
|
||||
startVm, stopVm, rebootVm, suspendVm, resumeVm,
|
||||
@@ -3692,6 +3697,8 @@ onMounted(() => { isPageActive = true; initPage() })
|
||||
.config-cell:last-child { border-right: none; }
|
||||
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
|
||||
.config-value { font-size: 14px; color: #1d2129; line-height: 1.4; word-break: break-all; }
|
||||
.cfg-edit-btn { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
|
||||
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
|
||||
.spec-value { font-size: 13px; color: #4e5969; }
|
||||
.ip-value { color: #165dff; font-weight: 500; }
|
||||
.password-cell { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
Reference in New Issue
Block a user