feat(admin/vm): 流量概览展示新字段 + 修改入口改挂 traffic_policy 接口
缘由:上次(802eaa3)我把概览"流量上限"的"修改"按钮挂到旧 update_traffic 接口
(兼容字段 traffic_max),与 docs/2026.05.08.12.37-add.json 中真正的流量策略接口
(traffic_policy/update + add_fixed + add_temporary)路径错位;同时 vm 详情返回
已新增 traffic_max_mb / temporary_traffic_mb / temporary_cycle_start / traffic_used /
traffic_exhausted_rx_mbps / traffic_exhausted_tx_mbps 等字段,概览未体现。
修改:
- UserVmDetail.vue & VmDetail.vue 概览将"流量上限"单值 cell 改为分段展示:
主行 已用/总量;副行 基础 + 临时(含周期起始日期);按钮组「修改」「加临时」。
- 主行/副行字段来源 add.json 新字段,旧 traffic_max 仅作 fallback。
- 「修改」按钮改挂 openTrafficPolicyDialog / openVmTrafficPolicyDialog
(对应 user_vm/traffic_policy/update 与 host_service/point/vm/traffic_policy/update);
「加临时」直达 openAddTrafficDialog('temporary') / openVmAddTrafficDialog('temporary')。
- openTrafficPolicyDialog / openVmTrafficPolicyDialog 增加 vm / detail 字段 fallback,
并在 trafficPolicy 未加载时异步触发 loadTrafficPolicy,避免懒加载导致初值全 0。
- 新增 formatTrafficMb helper(VmDetail.vue)处理 MB 自适应单位、对 0 友好输出。
- 新增 .traffic-cell 系列样式。
预期:
- 详情概览能直接看到 总/已用/基础/临时/周期 五个关键信息。
- 概览"修改"走 add.json 中的新流量策略接口,与"流量策略" tab 行为一致。
- 旧 dropdown 中"修改带宽"路径保留(不删除),用于纯带宽场景。
未测试:admin_dashboard_pc 本地 HMR 已更新,无编译/控制台报错。新流量策略接口与
真实 vm.value 字段填充尚需联调验证(特别是 traffic_used 单位假设为 MB,若实际为
字节需调整 formatTraffic / detailTrafficUsedMb 的换算)。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -130,12 +130,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-cell">
|
<div class="config-cell">
|
||||||
<span class="config-label">流量上限</span>
|
<span class="config-label">流量</span>
|
||||||
<span class="config-value">
|
<span class="config-value traffic-cell">
|
||||||
{{ formatTraffic(vm.traffic_max) }}
|
<span class="traffic-main">{{ formatTraffic(trafficUsedMb) }} / {{ formatTraffic(trafficTotalMb) }}</span>
|
||||||
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleMoreCmd('updateTraffic')">
|
<span class="traffic-sub">
|
||||||
<el-icon :size="14"><Edit /></el-icon>修改
|
基础 {{ formatTraffic(trafficBaseMb) }}
|
||||||
</el-button>
|
<template v-if="trafficTempMb > 0">
|
||||||
|
· 临时 {{ formatTraffic(trafficTempMb) }}
|
||||||
|
<span v-if="trafficCycleStartText" class="traffic-cycle">(周期 {{ trafficCycleStartText }})</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span class="traffic-actions">
|
||||||
|
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="openTrafficPolicyDialog">
|
||||||
|
<el-icon :size="14"><Edit /></el-icon>修改
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="warning" size="small" class="cfg-edit-btn" @click="openAddTrafficDialog('temporary')">加临时</el-button>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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.renewPrice / 100).toFixed(2) }}</span></div>
|
||||||
@@ -1180,6 +1190,18 @@ const copyAllIps = async (ipList) => {
|
|||||||
|
|
||||||
const isWindows = computed(() => vmImage.value?.os_type === 'windows')
|
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(() => {
|
const vmPublicIpList = computed(() => {
|
||||||
return vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.address ? n.address.split('/')[0] : n.name).filter(Boolean)
|
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 = () => {
|
const openTrafficPolicyDialog = () => {
|
||||||
|
// 从概览触发时 trafficPolicy 可能尚未加载(trafficPolicy tab 懒加载),
|
||||||
|
// 用 vm.value 上 add.json 新字段作 fallback;同时异步加载策略以便后续 submit 时数据准确
|
||||||
Object.assign(trafficPolicyForm, {
|
Object.assign(trafficPolicyForm, {
|
||||||
traffic_max_mb: trafficPolicy.value?.traffic_max_mb || 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 || 0,
|
exhausted_rx_mbps: trafficPolicy.value?.exhausted_rx_mbps ?? vm.value?.traffic_exhausted_rx_mbps ?? 0,
|
||||||
exhausted_tx_mbps: trafficPolicy.value?.exhausted_tx_mbps || 0
|
exhausted_tx_mbps: trafficPolicy.value?.exhausted_tx_mbps ?? vm.value?.traffic_exhausted_tx_mbps ?? 0
|
||||||
})
|
})
|
||||||
trafficPolicyVisible.value = true
|
trafficPolicyVisible.value = true
|
||||||
|
if (!trafficPolicy.value) loadTrafficPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitUpdateTrafficPolicy = async () => {
|
const submitUpdateTrafficPolicy = async () => {
|
||||||
@@ -2416,6 +2441,12 @@ onBeforeUnmount(() => { disposeCharts() })
|
|||||||
.config-value { font-size: 13px; color: #1d2129; line-height: 1.4; word-break: break-all; }
|
.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 { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
|
||||||
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
|
.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-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-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; }
|
.pwd-btn { padding: 0 !important; height: auto !important; min-height: auto !important; }
|
||||||
|
|||||||
@@ -221,12 +221,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<div class="config-cell">
|
<div class="config-cell">
|
||||||
<span class="config-label">流量上限</span>
|
<span class="config-label">流量</span>
|
||||||
<span class="config-value">
|
<span class="config-value traffic-cell">
|
||||||
{{ formatTrafficMax(detail.traffic_max) }}
|
<span class="traffic-main">{{ formatTrafficMb(detailTrafficUsedMb) }} / {{ formatTrafficMb(detailTrafficTotalMb) }}</span>
|
||||||
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleUpdateTraffic" :disabled="isMigrating">
|
<span class="traffic-sub">
|
||||||
<el-icon :size="14"><Edit /></el-icon>修改
|
基础 {{ formatTrafficMb(detailTrafficBaseMb) }}
|
||||||
</el-button>
|
<template v-if="detailTrafficTempMb > 0">
|
||||||
|
· 临时 {{ formatTrafficMb(detailTrafficTempMb) }}
|
||||||
|
<span v-if="detailTrafficCycleStartText" class="traffic-cycle">(周期 {{ detailTrafficCycleStartText }})</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span class="traffic-actions">
|
||||||
|
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="openVmTrafficPolicyDialog" :disabled="isMigrating">
|
||||||
|
<el-icon :size="14"><Edit /></el-icon>修改
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="warning" size="small" class="cfg-edit-btn" @click="openVmAddTrafficDialog('temporary')" :disabled="isMigrating">加临时</el-button>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-cell">
|
<div class="config-cell">
|
||||||
@@ -1649,6 +1659,33 @@ const formatTrafficMax = (val) => {
|
|||||||
return `${gb.toFixed(2)} GB`
|
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) => {
|
const copyAllIps = (ipList) => {
|
||||||
if (!ipList?.length) return
|
if (!ipList?.length) return
|
||||||
const text = ipList.join('\n')
|
const text = ipList.join('\n')
|
||||||
@@ -2292,12 +2329,15 @@ const loadVmTrafficPolicy = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openVmTrafficPolicyDialog = () => {
|
const openVmTrafficPolicyDialog = () => {
|
||||||
|
// 从概览触发时 vmTrafficPolicy 可能尚未加载(流量策略 tab 懒加载),
|
||||||
|
// 用 detail.value 上 add.json 新字段作 fallback;同时异步加载策略
|
||||||
Object.assign(vmTrafficPolicyForm, {
|
Object.assign(vmTrafficPolicyForm, {
|
||||||
traffic_max_mb: vmTrafficPolicy.value?.traffic_max_mb || 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 || 0,
|
exhausted_rx_mbps: vmTrafficPolicy.value?.exhausted_rx_mbps ?? detail.value?.traffic_exhausted_rx_mbps ?? 0,
|
||||||
exhausted_tx_mbps: vmTrafficPolicy.value?.exhausted_tx_mbps || 0
|
exhausted_tx_mbps: vmTrafficPolicy.value?.exhausted_tx_mbps ?? detail.value?.traffic_exhausted_tx_mbps ?? 0
|
||||||
})
|
})
|
||||||
vmTrafficPolicyVisible.value = true
|
vmTrafficPolicyVisible.value = true
|
||||||
|
if (!vmTrafficPolicy.value) loadVmTrafficPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitUpdateVmTrafficPolicy = async () => {
|
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; }
|
.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 { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
|
||||||
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
|
.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; }
|
.spec-value { font-size: 13px; color: #4e5969; }
|
||||||
.ip-value { color: #165dff; font-weight: 500; }
|
.ip-value { color: #165dff; font-weight: 500; }
|
||||||
.password-cell { display: flex; align-items: center; gap: 8px; }
|
.password-cell { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user