diff --git a/src/api/admin/kvmService.js b/src/api/admin/kvmService.js index e22e9fd..cfc26e3 100644 --- a/src/api/admin/kvmService.js +++ b/src/api/admin/kvmService.js @@ -401,6 +401,17 @@ export const updateVmTraffic = (data) => { }) } +// ========== 流量策略 ========== +// 测试未通过(接口新增,待联调) +/** 获取虚拟机流量策略 */ +export const getVmTrafficPolicy = (params) => http2.get('/api/v1/admin/server/host_service/point/vm/traffic_policy', { params }) +/** 修改虚拟机流量策略 */ +export const updateVmTrafficPolicy = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/update', data, { headers: { 'Content-Type': 'multipart/form-data' } }) +/** 增加虚拟机固定流量上限 */ +export const addVmFixedTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_fixed', data, { headers: { 'Content-Type': 'multipart/form-data' } }) +/** 增加虚拟机一次性临时流量 */ +export const addVmTemporaryTraffic = (data) => http2.post('/api/v1/admin/server/host_service/point/vm/traffic_policy/add_temporary', data, { headers: { 'Content-Type': 'multipart/form-data' } }) + /** 启动虚拟机 */ export const startVm = (data) => { return http2.post('/api/v1/admin/server/host_service/point/vm/start', data, { diff --git a/src/api/admin/userVm.js b/src/api/admin/userVm.js index 7433c42..0492175 100644 --- a/src/api/admin/userVm.js +++ b/src/api/admin/userVm.js @@ -106,6 +106,13 @@ export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params }) +// ========== 流量策略 ========== +// 测试未通过(接口新增,待联调) +export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params }) +export const updateUserVmTrafficPolicy = (data) => http2.post(`${BASE}/traffic_policy/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) +export const addUserVmFixedTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_fixed`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) +export const addUserVmTemporaryTraffic = (data) => http2.post(`${BASE}/traffic_policy/add_temporary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) + // ========== 到期提醒 ========== export const getExpireRemindList = (params) => http2.get(`${GOODS_BASE}/expire_remind/list`, { params }) export const sendExpireRemind = (data) => http2.post(`${GOODS_BASE}/expire_remind/send`, data, { headers: { 'Content-Type': 'application/json' } }) diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue index b9cc358..3c066f4 100644 --- a/src/views/user-vm/UserVmDetail.vue +++ b/src/views/user-vm/UserVmDetail.vue @@ -434,6 +434,27 @@ + + +
+
+

流量策略

+
+ 修改流量策略 + 增加固定流量 + 增加临时流量 + 刷新 +
+
+ + {{ trafficPolicy.traffic_max_mb != null ? (trafficPolicy.traffic_max_mb === 0 ? '不限' : trafficPolicy.traffic_max_mb + ' MB') : '-' }} + {{ trafficPolicy.exhausted_rx_mbps != null ? (trafficPolicy.exhausted_rx_mbps === 0 ? '不限' : trafficPolicy.exhausted_rx_mbps + ' Mbps') : '-' }} + {{ trafficPolicy.exhausted_tx_mbps != null ? (trafficPolicy.exhausted_tx_mbps === 0 ? '不限' : trafficPolicy.exhausted_tx_mbps + ' Mbps') : '-' }} + + +
+
+ @@ -647,6 +668,18 @@ + +
+ + Mbps(0 不限) +
+
+ +
+ + Mbps(0 不限) +
+
+ + + + +
+ + MB(0 不限) +
+
+ +
+ + Mbps(0 不限) +
+
+ +
+ + Mbps(0 不限) +
+
+
+ +
+ + + + + +
+ + MB +
+
+
+ +
+ @@ -1008,7 +1085,8 @@ import { getUserVmPostGroupDetail, getUserVmNetworkList, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking, getUserGoodsDetail, - getUserVmMetricsHistory + getUserVmMetricsHistory, + getUserVmTrafficPolicy, updateUserVmTrafficPolicy, addUserVmFixedTraffic, addUserVmTemporaryTraffic } from '@/api/admin/userVm' import { extractApiError } from '@/utils/kvmErrorUtil' import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool' @@ -1184,6 +1262,7 @@ const handleTabChange = (tab) => { if (tab === 'security') loadSgLockInfo() if (tab === 'networking') loadNetworkings() if (tab === 'monitor' && !metricsData.value) loadMetricsHistory() + if (tab === 'trafficPolicy') loadTrafficPolicy() } // 请求安全组详情补充 lock 字段(使用用户虚拟机安全组详情接口) @@ -1234,7 +1313,7 @@ const submitPower = async () => { const handleMoreCmd = (cmd) => { if (powerLabels[cmd]) { handlePower(cmd); return } if (cmd === 'rebuild') { rebuildImageId.value = 0; rebuildImageName.value = ''; rebuildImages.value = []; rebuildVisible.value = true; loadRebuildImages() } - if (cmd === 'updateTraffic') { Object.assign(trafficForm, { rx_bandwidth: vm.value?.rx_bandwidth || 0, tx_bandwidth: vm.value?.tx_bandwidth || 0, _trafficValue: +((vm.value?.traffic_max || 0) / 1024).toFixed(2), _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB' }); trafficVisible.value = true } + if (cmd === 'updateTraffic') { Object.assign(trafficForm, { rx_bandwidth: vm.value?.rx_bandwidth || 0, tx_bandwidth: vm.value?.tx_bandwidth || 0, _trafficValue: +((vm.value?.traffic_max || 0) / 1024).toFixed(2), _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB', traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }); trafficVisible.value = true } if (cmd === 'transfer') { Object.assign(transferForm, { target_user_id: 0, _userName: '' }); transferVisible.value = true } if (cmd === 'updateVm') openEditVm() if (cmd === 'refactorVm') openRefactorVm() @@ -1897,7 +1976,7 @@ const submitEditVm = async () => { // ---- 修改带宽 ---- const trafficVisible = ref(false) -const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficValue: 0, _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB' }) +const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficValue: 0, _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB', traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }) const submitUpdateTraffic = async () => { actionLoading.value = true try { @@ -1912,12 +1991,73 @@ const submitUpdateTraffic = async () => { tx_bandwidth: Math.round(txBw), traffic_max: Math.round(trafficMb) } + if (trafficForm.traffic_exhausted_rx_mbps > 0) payload.traffic_exhausted_rx_mbps = trafficForm.traffic_exhausted_rx_mbps + if (trafficForm.traffic_exhausted_tx_mbps > 0) payload.traffic_exhausted_tx_mbps = trafficForm.traffic_exhausted_tx_mbps const res = await updateUserVmTraffic(payload) if (res?.data?.code === 200) { ElMessage.success('修改成功'); trafficVisible.value = false; loadDetail() } else ElMessage.error(extractApiError(res?.data, '修改失败')) } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false } } +// ---- 流量策略 ---- +// 测试未通过(接口新增,待联调) +const trafficPolicy = ref(null) +const trafficPolicyLoading = ref(false) +const trafficPolicyVisible = ref(false) +const addTrafficVisible = ref(false) +const addTrafficType = ref('fixed') +const trafficPolicyForm = reactive({ traffic_max_mb: 0, exhausted_rx_mbps: 0, exhausted_tx_mbps: 0 }) +const addTrafficForm = reactive({ traffic_mb: 1 }) + +const loadTrafficPolicy = async () => { + if (!userGoodsId.value) return + trafficPolicyLoading.value = true + try { + const res = await getUserVmTrafficPolicy({ user_goods_id: userGoodsId.value }) + if (res?.data?.code === 200) trafficPolicy.value = res.data.data + } catch { /* ignore */ } finally { trafficPolicyLoading.value = false } +} + +const openTrafficPolicyDialog = () => { + Object.assign(trafficPolicyForm, { + traffic_max_mb: trafficPolicy.value?.traffic_max_mb || 0, + exhausted_rx_mbps: trafficPolicy.value?.exhausted_rx_mbps || 0, + exhausted_tx_mbps: trafficPolicy.value?.exhausted_tx_mbps || 0 + }) + trafficPolicyVisible.value = true +} + +const submitUpdateTrafficPolicy = async () => { + trafficPolicyLoading.value = true + try { + const res = await updateUserVmTrafficPolicy({ + user_goods_id: userGoodsId.value, + traffic_max_mb: trafficPolicyForm.traffic_max_mb, + exhausted_rx_mbps: trafficPolicyForm.exhausted_rx_mbps, + exhausted_tx_mbps: trafficPolicyForm.exhausted_tx_mbps + }) + if (res?.data?.code === 200) { ElMessage.success('修改成功'); trafficPolicyVisible.value = false; loadTrafficPolicy() } + else ElMessage.error(extractApiError(res?.data, '修改失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { trafficPolicyLoading.value = false } +} + +const openAddTrafficDialog = (type) => { + addTrafficType.value = type + addTrafficForm.traffic_mb = 1 + addTrafficVisible.value = true +} + +const submitAddTraffic = async () => { + if (!addTrafficForm.traffic_mb || addTrafficForm.traffic_mb < 1) { ElMessage.warning('请输入有效的流量数量'); return } + trafficPolicyLoading.value = true + try { + const apiFn = addTrafficType.value === 'fixed' ? addUserVmFixedTraffic : addUserVmTemporaryTraffic + const res = await apiFn({ user_goods_id: userGoodsId.value, traffic_mb: addTrafficForm.traffic_mb }) + if (res?.data?.code === 200) { ElMessage.success('操作成功'); addTrafficVisible.value = false; loadTrafficPolicy() } + else ElMessage.error(extractApiError(res?.data, '操作失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { trafficPolicyLoading.value = false } +} + // ---- 转移 ---- const transferVisible = ref(false) const showTransferUserSelector = ref(false) diff --git a/src/views/user-vm/UserVmList.vue b/src/views/user-vm/UserVmList.vue index bd4807a..fad4a52 100644 --- a/src/views/user-vm/UserVmList.vue +++ b/src/views/user-vm/UserVmList.vue @@ -273,6 +273,40 @@ + + + + +
+ + MB +
+
0 表示不限流量
+
+
+ + +
+ + + +
+ + Mbps +
+
流量耗尽后下行速率,0 表示不限
+
+
+ + +
+ + Mbps +
+
流量耗尽后上行速率,0 表示不限
+
+
+
@@ -1090,7 +1124,8 @@ const createForm = reactive({ network_ids: [], _networkNames: '', ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0, _renewPriceYuan: 0, _basePriceYuan: 0, - data_volume_size: 0, note: '', expire_time: '' + data_volume_size: 0, note: '', expire_time: '', + traffic_max: 0, traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }) const createRules = { @@ -1116,7 +1151,8 @@ const handleCreate = (tab = 'create') => { host_id: 0, _hostName: '', host_group_id: 0, _hostGroupName: '', network_ids: [], _networkNames: '', ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0, - _renewPriceYuan: 0, _basePriceYuan: 0, data_volume_size: 0, note: '', expire_time: '' + _renewPriceYuan: 0, _basePriceYuan: 0, data_volume_size: 0, note: '', expire_time: '', + traffic_max: 0, traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }) resetBindGoodsForm() pendingTab = tab @@ -1213,6 +1249,9 @@ const submitCreate = () => { if (createForm.host_id) payload.host_id = createForm.host_id if (createForm.host_group_id) payload.host_group_id = createForm.host_group_id if (createForm.network_ids.length) payload.network_ids = createForm.network_ids + if (createForm.traffic_max > 0) payload.traffic_max = createForm.traffic_max + if (createForm.traffic_exhausted_rx_mbps > 0) payload.traffic_exhausted_rx_mbps = createForm.traffic_exhausted_rx_mbps + if (createForm.traffic_exhausted_tx_mbps > 0) payload.traffic_exhausted_tx_mbps = createForm.traffic_exhausted_tx_mbps const res = await createUserVm(payload) if (res?.data?.code === 200) { ElMessage.success('创建成功'); createVisible.value = false; loadList() } else ElMessage.error(extractApiError(res?.data, '创建失败')) diff --git a/src/views/virtualization/VmDetail.vue b/src/views/virtualization/VmDetail.vue index e197d7a..7ff8cd6 100644 --- a/src/views/virtualization/VmDetail.vue +++ b/src/views/virtualization/VmDetail.vue @@ -641,6 +641,28 @@ + + + +
+
+

流量策略

+
+ 修改流量策略 + 增加固定流量 + 增加临时流量 + 刷新 +
+
+ + {{ vmTrafficPolicy.traffic_max_mb != null ? (vmTrafficPolicy.traffic_max_mb === 0 ? '不限' : vmTrafficPolicy.traffic_max_mb + ' MB') : '-' }} + {{ vmTrafficPolicy.exhausted_rx_mbps != null ? (vmTrafficPolicy.exhausted_rx_mbps === 0 ? '不限' : vmTrafficPolicy.exhausted_rx_mbps + ' Mbps') : '-' }} + {{ vmTrafficPolicy.exhausted_tx_mbps != null ? (vmTrafficPolicy.exhausted_tx_mbps === 0 ? '不限' : vmTrafficPolicy.exhausted_tx_mbps + ' Mbps') : '-' }} + + +
+
+ @@ -958,6 +980,16 @@ GB + +
+ Mbps(0 不限) +
+
+ +
+ Mbps(0 不限) +
+
+ + + + +
+ MB(0 不限) +
+
+ +
+ Mbps(0 不限) +
+
+ +
+ Mbps(0 不限) +
+
+
+ +
+ + + + + +
+ MB +
+
+
+ +
+ @@ -1506,7 +1582,8 @@ import { getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit, migrateVm, getRemoteHostGroupList, getRemoteHostDetail, dataMigrateVm, getDataMigrateProgress, abortDataMigrate, - getKvmServiceList, getMetricsHistory, getNetworkList + getKvmServiceList, getMetricsHistory, getNetworkList, + getVmTrafficPolicy, updateVmTrafficPolicy, addVmFixedTraffic, addVmTemporaryTraffic } from '@/api/admin/kvmService' import { getUserInfo } from '@/api/admin/user' import { extractApiError } from '@/utils/kvmErrorUtil' @@ -2159,14 +2236,16 @@ const submitRefactorVm = async () => { // ---- 修改带宽 ---- const trafficDialogVisible = ref(false) -const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0 }) +const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0, traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }) const handleUpdateTraffic = () => { if (!detail.value) return Object.assign(trafficForm, { rx_bandwidth: detail.value.rx_bandwidth || 0, tx_bandwidth: detail.value.tx_bandwidth || 0, - _trafficGB: ((detail.value.traffic_max || 0) / 1024).toFixed(2) * 1 + _trafficGB: ((detail.value.traffic_max || 0) / 1024).toFixed(2) * 1, + traffic_exhausted_rx_mbps: 0, + traffic_exhausted_tx_mbps: 0 }) trafficDialogVisible.value = true } @@ -2180,12 +2259,78 @@ const submitUpdateTraffic = async () => { fd.append('rx_bandwidth', trafficForm.rx_bandwidth) fd.append('tx_bandwidth', trafficForm.tx_bandwidth) if (trafficForm._trafficGB) fd.append('traffic_max', Math.round(trafficForm._trafficGB * 1024)) // GB → Mb + if (trafficForm.traffic_exhausted_rx_mbps > 0) fd.append('traffic_exhausted_rx_mbps', trafficForm.traffic_exhausted_rx_mbps) + if (trafficForm.traffic_exhausted_tx_mbps > 0) fd.append('traffic_exhausted_tx_mbps', trafficForm.traffic_exhausted_tx_mbps) const res = await updateVmTraffic(fd) if (res?.data?.code === 200) { ElMessage.success('带宽修改成功'); trafficDialogVisible.value = false; loadDetail() } else ElMessage.error(extractApiError(res?.data, '修改失败')) } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false } } +// ---- 流量策略 ---- +// 测试未通过(接口新增,待联调) +const vmTrafficPolicy = ref(null) +const vmTrafficPolicyLoading = ref(false) +const vmTrafficPolicyVisible = ref(false) +const vmAddTrafficVisible = ref(false) +const vmAddTrafficType = ref('fixed') +const vmTrafficPolicyForm = reactive({ traffic_max_mb: 0, exhausted_rx_mbps: 0, exhausted_tx_mbps: 0 }) +const vmAddTrafficForm = reactive({ traffic_mb: 1 }) + +const loadVmTrafficPolicy = async () => { + if (!serviceId.value || !vmId.value) return + vmTrafficPolicyLoading.value = true + try { + const res = await getVmTrafficPolicy({ service_id: serviceId.value, vm_id: vmId.value }) + if (res?.data?.code === 200) vmTrafficPolicy.value = res.data.data + } catch { /* ignore */ } finally { vmTrafficPolicyLoading.value = false } +} + +const openVmTrafficPolicyDialog = () => { + Object.assign(vmTrafficPolicyForm, { + traffic_max_mb: vmTrafficPolicy.value?.traffic_max_mb || 0, + exhausted_rx_mbps: vmTrafficPolicy.value?.exhausted_rx_mbps || 0, + exhausted_tx_mbps: vmTrafficPolicy.value?.exhausted_tx_mbps || 0 + }) + vmTrafficPolicyVisible.value = true +} + +const submitUpdateVmTrafficPolicy = async () => { + vmTrafficPolicyLoading.value = true + try { + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('vm_id', vmId.value) + fd.append('traffic_max_mb', vmTrafficPolicyForm.traffic_max_mb) + fd.append('exhausted_rx_mbps', vmTrafficPolicyForm.exhausted_rx_mbps) + fd.append('exhausted_tx_mbps', vmTrafficPolicyForm.exhausted_tx_mbps) + const res = await updateVmTrafficPolicy(fd) + if (res?.data?.code === 200) { ElMessage.success('修改成功'); vmTrafficPolicyVisible.value = false; loadVmTrafficPolicy() } + else ElMessage.error(extractApiError(res?.data, '修改失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { vmTrafficPolicyLoading.value = false } +} + +const openVmAddTrafficDialog = (type) => { + vmAddTrafficType.value = type + vmAddTrafficForm.traffic_mb = 1 + vmAddTrafficVisible.value = true +} + +const submitAddVmTraffic = async () => { + if (!vmAddTrafficForm.traffic_mb || vmAddTrafficForm.traffic_mb < 1) { ElMessage.warning('请输入有效的流量数量'); return } + vmTrafficPolicyLoading.value = true + try { + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('vm_id', vmId.value) + fd.append('traffic_mb', vmAddTrafficForm.traffic_mb) + const apiFn = vmAddTrafficType.value === 'fixed' ? addVmFixedTraffic : addVmTemporaryTraffic + const res = await apiFn(fd) + if (res?.data?.code === 200) { ElMessage.success('操作成功'); vmAddTrafficVisible.value = false; loadVmTrafficPolicy() } + else ElMessage.error(extractApiError(res?.data, '操作失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { vmTrafficPolicyLoading.value = false } +} + // ---- 迁移虚拟机 ---- const migrateDialogVisible = ref(false) const migrateOptionsLoading = ref(false) @@ -3474,6 +3619,7 @@ const triggerTabLoad = (tab) => { if (tab === 'backup') { loadBackups(); loadBackupQuota() } if (tab === 'userNetworking') loadVmNetworkingList() if (tab === 'security') loadSgLockInfo() + if (tab === 'vmTrafficPolicy') loadVmTrafficPolicy() } // 请求安全组详情补充 lock 字段 diff --git a/src/views/virtualization/VmManage.vue b/src/views/virtualization/VmManage.vue index 7b9e879..2653056 100644 --- a/src/views/virtualization/VmManage.vue +++ b/src/views/virtualization/VmManage.vue @@ -176,6 +176,16 @@ Mbps + + MB +
0 表示不限流量
+
+ + Mbps(0 不限) + + + Mbps(0 不限) +
@@ -616,7 +626,8 @@ const createForm = reactive({ name: '', host_id: null, image_id: 0, vcpu: 0, memory: 0, system_size: 0, rx_bandwidth: 0, tx_bandwidth: 0, data_volume_size: 0, host_group_id: null, user_id: 0, ipv4_num: 0, ipv6_num: 0, network_ids: [], - _imageName: '', _groupName: '', _userName: '' + _imageName: '', _groupName: '', _userName: '', + traffic_max: 0, traffic_exhausted_rx_mbps: 0, traffic_exhausted_tx_mbps: 0 }) const createRules = { @@ -802,6 +813,9 @@ const submitCreate = () => { if (createForm.ipv4_num) fd.append('ipv4_num', createForm.ipv4_num) if (createForm.ipv6_num) fd.append('ipv6_num', createForm.ipv6_num) } else createForm.network_ids.forEach(id => fd.append('network_ids', id)) + if (createForm.traffic_max > 0) fd.append('traffic_max', createForm.traffic_max) + if (createForm.traffic_exhausted_rx_mbps > 0) fd.append('traffic_exhausted_rx_mbps', createForm.traffic_exhausted_rx_mbps) + if (createForm.traffic_exhausted_tx_mbps > 0) fd.append('traffic_exhausted_tx_mbps', createForm.traffic_exhausted_tx_mbps) const res = await createVm(fd) if (res?.data?.code === 200) { ElMessage.success('创建成功'); createDialogVisible.value = false; loadList() } else ElMessage.error(extractApiError(res?.data, '创建失败'))