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:
shiran
2026-05-12 17:09:23 +08:00
parent 3d783cd224
commit 802eaa396b
2 changed files with 73 additions and 5 deletions
+64 -3
View File
@@ -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; }
+9 -2
View File
@@ -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; }