diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue
index 7cd867c..d7124d9 100644
--- a/src/views/user-vm/UserVmDetail.vue
+++ b/src/views/user-vm/UserVmDetail.vue
@@ -130,12 +130,22 @@
- 流量上限
-
- {{ formatTraffic(vm.traffic_max) }}
-
- 修改
-
+ 流量
+
+ {{ formatTraffic(trafficUsedMb) }} / {{ formatTraffic(trafficTotalMb) }}
+
+ 基础 {{ formatTraffic(trafficBaseMb) }}
+
+ · 临时 {{ formatTraffic(trafficTempMb) }}
+ (周期 {{ trafficCycleStartText }})
+
+
+
+
+ 修改
+
+ 加临时
+
续费价格¥{{ (userGoods.renewPrice / 100).toFixed(2) }}
@@ -1180,6 +1190,18 @@ const copyAllIps = async (ipList) => {
const isWindows = computed(() => vmImage.value?.os_type === 'windows')
+// 流量字段优先使用 traffic_max_mb(add.json 新字段),fallback 旧字段 traffic_max
+// traffic_used 单位假设为 MB(与 traffic_max_mb 同维度),如后端实为字节需调整
+const trafficBaseMb = computed(() => Number(vm.value?.traffic_max_mb ?? vm.value?.traffic_max ?? 0) || 0)
+const trafficTempMb = computed(() => Number(vm.value?.temporary_traffic_mb || 0))
+const trafficTotalMb = computed(() => trafficBaseMb.value + trafficTempMb.value)
+const trafficUsedMb = computed(() => Number(vm.value?.traffic_used || 0))
+const trafficCycleStartText = computed(() => {
+ const t = vm.value?.temporary_cycle_start
+ const sec = typeof t === 'object' ? (t?.seconds ?? null) : t
+ return sec ? dayjs(Number(sec) * 1000).format('YYYY-MM-DD') : ''
+})
+
const vmPublicIpList = computed(() => {
return vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.address ? n.address.split('/')[0] : n.name).filter(Boolean)
})
@@ -2078,12 +2100,15 @@ const loadTrafficPolicy = async () => {
}
const openTrafficPolicyDialog = () => {
+ // 从概览触发时 trafficPolicy 可能尚未加载(trafficPolicy tab 懒加载),
+ // 用 vm.value 上 add.json 新字段作 fallback;同时异步加载策略以便后续 submit 时数据准确
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
+ traffic_max_mb: trafficPolicy.value?.traffic_max_mb ?? vm.value?.traffic_max_mb ?? vm.value?.traffic_max ?? 0,
+ exhausted_rx_mbps: trafficPolicy.value?.exhausted_rx_mbps ?? vm.value?.traffic_exhausted_rx_mbps ?? 0,
+ exhausted_tx_mbps: trafficPolicy.value?.exhausted_tx_mbps ?? vm.value?.traffic_exhausted_tx_mbps ?? 0
})
trafficPolicyVisible.value = true
+ if (!trafficPolicy.value) loadTrafficPolicy()
}
const submitUpdateTrafficPolicy = async () => {
@@ -2416,6 +2441,12 @@ onBeforeUnmount(() => { disposeCharts() })
.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; }
+.traffic-cell { display: flex; flex-direction: column; gap: 2px; }
+.traffic-cell .traffic-main { font-size: 13px; color: #1d2129; }
+.traffic-cell .traffic-sub { font-size: 12px; color: #86909c; line-height: 1.3; }
+.traffic-cell .traffic-cycle { color: #c0c4cc; margin-left: 2px; }
+.traffic-cell .traffic-actions { display: inline-flex; gap: 6px; margin-top: 2px; }
+.traffic-cell .traffic-actions .cfg-edit-btn { margin-left: 0; }
.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; }
diff --git a/src/views/virtualization/VmDetail.vue b/src/views/virtualization/VmDetail.vue
index dbbfc1d..21d80f4 100644
--- a/src/views/virtualization/VmDetail.vue
+++ b/src/views/virtualization/VmDetail.vue
@@ -221,12 +221,22 @@
- 流量上限
-
- {{ formatTrafficMax(detail.traffic_max) }}
-
- 修改
-
+ 流量
+
+ {{ formatTrafficMb(detailTrafficUsedMb) }} / {{ formatTrafficMb(detailTrafficTotalMb) }}
+
+ 基础 {{ formatTrafficMb(detailTrafficBaseMb) }}
+
+ · 临时 {{ formatTrafficMb(detailTrafficTempMb) }}
+ (周期 {{ detailTrafficCycleStartText }})
+
+
+
+
+ 修改
+
+ 加临时
+
@@ -1649,6 +1659,33 @@ const formatTrafficMax = (val) => {
return `${gb.toFixed(2)} GB`
}
+// 流量分段展示:以 MB 输入,自适应输出单位;与 formatTrafficMax 区别是允许 0 / 小数值精简显示
+const formatTrafficMb = (mb) => {
+ const n = Number(mb)
+ if (!Number.isFinite(n) || n <= 0) return '0 MB'
+ if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(2)} TB`
+ if (n >= 1024) return `${(n / 1024).toFixed(2)} GB`
+ return `${n} MB`
+}
+
+// 概览流量段:优先使用 add.json 新字段 traffic_max_mb / temporary_traffic_mb,fallback 旧字段 traffic_max
+// traffic_used 单位假设为 MB(与 traffic_max_mb 同维度),如后端实为字节需调整
+const detailTrafficBaseMb = computed(() => Number(detail.value?.traffic_max_mb ?? detail.value?.traffic_max ?? 0) || 0)
+const detailTrafficTempMb = computed(() => Number(detail.value?.temporary_traffic_mb || 0))
+const detailTrafficTotalMb = computed(() => detailTrafficBaseMb.value + detailTrafficTempMb.value)
+const detailTrafficUsedMb = computed(() => Number(detail.value?.traffic_used || 0))
+const detailTrafficCycleStartText = computed(() => {
+ const t = detail.value?.temporary_cycle_start
+ const sec = typeof t === 'object' ? (t?.seconds ?? null) : t
+ if (!sec) return ''
+ const d = new Date(Number(sec) * 1000)
+ if (isNaN(d.getTime())) return ''
+ const yyyy = d.getFullYear()
+ const mm = String(d.getMonth() + 1).padStart(2, '0')
+ const dd = String(d.getDate()).padStart(2, '0')
+ return `${yyyy}-${mm}-${dd}`
+})
+
const copyAllIps = (ipList) => {
if (!ipList?.length) return
const text = ipList.join('\n')
@@ -2292,12 +2329,15 @@ const loadVmTrafficPolicy = async () => {
}
const openVmTrafficPolicyDialog = () => {
+ // 从概览触发时 vmTrafficPolicy 可能尚未加载(流量策略 tab 懒加载),
+ // 用 detail.value 上 add.json 新字段作 fallback;同时异步加载策略
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
+ traffic_max_mb: vmTrafficPolicy.value?.traffic_max_mb ?? detail.value?.traffic_max_mb ?? detail.value?.traffic_max ?? 0,
+ exhausted_rx_mbps: vmTrafficPolicy.value?.exhausted_rx_mbps ?? detail.value?.traffic_exhausted_rx_mbps ?? 0,
+ exhausted_tx_mbps: vmTrafficPolicy.value?.exhausted_tx_mbps ?? detail.value?.traffic_exhausted_tx_mbps ?? 0
})
vmTrafficPolicyVisible.value = true
+ if (!vmTrafficPolicy.value) loadVmTrafficPolicy()
}
const submitUpdateVmTrafficPolicy = async () => {
@@ -3699,6 +3739,12 @@ onMounted(() => { isPageActive = true; initPage() })
.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; }
+.traffic-cell { display: flex; flex-direction: column; gap: 2px; }
+.traffic-cell .traffic-main { font-size: 14px; color: #1d2129; }
+.traffic-cell .traffic-sub { font-size: 12px; color: #86909c; line-height: 1.3; }
+.traffic-cell .traffic-cycle { color: #c0c4cc; margin-left: 2px; }
+.traffic-cell .traffic-actions { display: inline-flex; gap: 6px; margin-top: 2px; }
+.traffic-cell .traffic-actions .cfg-edit-btn { margin-left: 0; }
.spec-value { font-size: 13px; color: #4e5969; }
.ip-value { color: #165dff; font-weight: 500; }
.password-cell { display: flex; align-items: center; gap: 8px; }