fix: 修改新增用户商品的配置项逻辑
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
|
||||
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
|
||||
<el-dropdown-item divided command="migrateVm">迁移虚拟机</el-dropdown-item>
|
||||
<el-dropdown-item command="dataMigrateVm">数据迁移</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -225,7 +226,7 @@
|
||||
</el-table-column> -->
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status === 'ready' ? '就绪' : (row.status || '-') }}</el-tag>
|
||||
<el-tag :type="volumeStatusType(row.status)" size="small">{{ volumeStatusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="160" show-overflow-tooltip>
|
||||
@@ -259,7 +260,7 @@
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button size="small" type="primary" @click="handleSgCreate"><el-icon><Plus /></el-icon>创建安全组</el-button>
|
||||
<el-button size="small" @click="handleSgBind">绑定安全组</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="async () => { await loadDetail(); loadSgLockInfo() }">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="pagedSecurityGroups" size="small" stripe>
|
||||
@@ -789,8 +790,8 @@
|
||||
<el-form-item label="上行带宽(Mbps)">
|
||||
<el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="每月最大流量(MB)">
|
||||
<el-input-number v-model="trafficForm.traffic_max" :min="0" controls-position="right" style="width: 100%" />
|
||||
<el-form-item label="流量上限(GB)">
|
||||
<el-input-number v-model="trafficForm._trafficGB" :min="0" :precision="2" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -841,6 +842,66 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 数据迁移弹窗 -->
|
||||
<el-dialog v-model="dataMigrateVisible" title="发起虚拟机数据迁移" width="580px" destroy-on-close>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:16px">
|
||||
源服务的虚拟机数据将通过 rsync 传输到目标服务的宿主机上,完成后在目标宿主机上校验并恢复虚拟机。
|
||||
</el-alert>
|
||||
<el-form :model="dataMigrateForm" label-width="130px" v-loading="dataMigrateLoading">
|
||||
<el-form-item label="源主控服务">
|
||||
<el-input :model-value="`${serviceName} (ID: ${serviceId})`" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="源虚拟机">
|
||||
<el-input :model-value="`${detail?.name || ''} (ID: ${vmId})`" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标主控服务" required>
|
||||
<el-select v-model="dataMigrateForm.target_service_id" placeholder="选择目标主控服务" filterable style="width:100%"
|
||||
@change="dataMigrateForm.target_host_id = null; loadDataMigrateHosts()">
|
||||
<el-option v-for="s in dataMigrateServiceOptions" :key="s.id" :label="`${s.name} (ID:${s.id})`" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标宿主机" required>
|
||||
<el-select v-model="dataMigrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width:100%"
|
||||
:disabled="!dataMigrateForm.target_service_id" :loading="dataMigrateHostsLoading">
|
||||
<el-option v-for="h in dataMigrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="IPv4数量">
|
||||
<el-input-number v-model="dataMigrateForm.ipv4_num" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="IPv6数量">
|
||||
<el-input-number v-model="dataMigrateForm.ipv6_num" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dataMigrateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="actionLoading" @click="submitDataMigrate">发起迁移</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 数据迁移进度弹窗 -->
|
||||
<el-dialog v-model="dataMigrateProgressVisible" title="数据迁移进度" width="480px" destroy-on-close>
|
||||
<div v-loading="dataMigrateProgressLoading">
|
||||
<el-descriptions :column="1" border size="small" v-if="dataMigrateProgressData">
|
||||
<el-descriptions-item label="迁移ID"><span style="font-family:monospace;font-size:12px">{{ dataMigrateProgressData.migration_id || '-' }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="状态"><el-tag :type="taskStatusType(dataMigrateProgressData.status)" size="small">{{ dataMigrateProgressData.status || '-' }}</el-tag></el-descriptions-item>
|
||||
<el-descriptions-item label="进度" v-if="dataMigrateProgressData.progress != null">{{ dataMigrateProgressData.progress }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="信息" v-if="dataMigrateProgressData.message">{{ dataMigrateProgressData.message }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else-if="!dataMigrateProgressLoading" description="暂无进度信息" :image-size="60" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="dataMigrateProgressVisible = false">关闭</el-button>
|
||||
<el-button :icon="Refresh" @click="loadDataMigrateProgress" :loading="dataMigrateProgressLoading">刷新进度</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 绑定外网选择器(bridge) -->
|
||||
<NetworkSelectorPopup v-model="showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" />
|
||||
<!-- 绑定内网选择器(nat) -->
|
||||
@@ -1175,10 +1236,13 @@ import {
|
||||
getVmList, bindSecurityGroup, unbindSecurityGroup,
|
||||
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
|
||||
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
|
||||
migrateVm, getRemoteHostGroupList, getRemoteHostDetail
|
||||
migrateVm, getRemoteHostGroupList, getRemoteHostDetail,
|
||||
dataMigrateVm, getDataMigrateProgress,
|
||||
getKvmServiceList
|
||||
} from '@/api/admin/kvmService'
|
||||
import { getUserInfo } from '@/api/admin/user'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
|
||||
import * as echarts from 'echarts'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
||||
@@ -1264,10 +1328,11 @@ const handleMoreCommand = (cmd) => {
|
||||
migrateVm: handleMigrateVm
|
||||
}
|
||||
if (actionMap[cmd]) actionMap[cmd]()
|
||||
if (cmd === 'dataMigrateVm') handleDataMigrateVm()
|
||||
}
|
||||
|
||||
const vmStatusType = (s) => ({ running: 'success', ready: 'success', creating: 'warning', pending: 'info', stopped: 'danger', stop: 'danger', shutoff: 'danger', error: 'danger', paused: 'warning', reboot: 'warning', poweroff: 'info', unknown: 'info' }[s] || 'info')
|
||||
const vmStatusLabel = (s) => ({ running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中', stopped: '已停止', stop: '已停止', shutoff: '已关闭', error: '错误', paused: '已暂停', reboot: '重启中', poweroff: '已关机', unknown: '未知' }[s] || s || '-')
|
||||
const vmStatusType = (s) => vmStatusTypeUtil(s)
|
||||
const vmStatusLabel = (s) => vmStatusLabelUtil(s)
|
||||
const imgStatusType = (s) => ({ ready: 'success', downloading: 'warning', pending: 'info', error: 'danger' }[s] || 'info')
|
||||
const imgStatusLabel = (s) => ({ ready: '就绪', downloading: '下载中', pending: '等待中', error: '错误' }[s] || s || '-')
|
||||
|
||||
@@ -1696,14 +1761,14 @@ const submitRefactorVm = async () => {
|
||||
|
||||
// ---- 修改带宽 ----
|
||||
const trafficDialogVisible = ref(false)
|
||||
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, traffic_max: 0 })
|
||||
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0 })
|
||||
|
||||
const handleUpdateTraffic = () => {
|
||||
if (!detail.value) return
|
||||
Object.assign(trafficForm, {
|
||||
rx_bandwidth: detail.value.rx_bandwidth || 0,
|
||||
tx_bandwidth: detail.value.tx_bandwidth || 0,
|
||||
traffic_max: detail.value.traffic_max || 0
|
||||
_trafficGB: ((detail.value.traffic_max || 0) / 1024).toFixed(2) * 1
|
||||
})
|
||||
trafficDialogVisible.value = true
|
||||
}
|
||||
@@ -1716,7 +1781,7 @@ const submitUpdateTraffic = async () => {
|
||||
fd.append('vm_id', vmId.value)
|
||||
fd.append('rx_bandwidth', trafficForm.rx_bandwidth)
|
||||
fd.append('tx_bandwidth', trafficForm.tx_bandwidth)
|
||||
if (trafficForm.traffic_max) fd.append('traffic_max', trafficForm.traffic_max)
|
||||
if (trafficForm._trafficGB) fd.append('traffic_max', Math.round(trafficForm._trafficGB * 1024)) // GB → Mb
|
||||
const res = await updateVmTraffic(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('带宽修改成功'); trafficDialogVisible.value = false; loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '修改失败'))
|
||||
@@ -1770,6 +1835,78 @@ const submitMigrateVm = async () => {
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- 数据迁移 ----
|
||||
const dataMigrateVisible = ref(false)
|
||||
const dataMigrateLoading = ref(false)
|
||||
const dataMigrateHostsLoading = ref(false)
|
||||
const dataMigrateServiceOptions = ref([])
|
||||
const dataMigrateHostOptions = ref([])
|
||||
const dataMigrateProgressVisible = ref(false)
|
||||
const dataMigrateProgressLoading = ref(false)
|
||||
const dataMigrateProgressData = ref(null)
|
||||
const dataMigrationId = ref('')
|
||||
const dataMigrateForm = reactive({ target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||
|
||||
const handleDataMigrateVm = async () => {
|
||||
Object.assign(dataMigrateForm, { target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||
dataMigrateVisible.value = true
|
||||
dataMigrateLoading.value = true
|
||||
try {
|
||||
const res = await getKvmServiceList({ page: 1, count: 10 })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||
dataMigrateServiceOptions.value = raw.map(s => ({ id: s.id ?? s.Id, name: s.name ?? s.Name }))
|
||||
}
|
||||
} catch { /* */ } finally { dataMigrateLoading.value = false }
|
||||
}
|
||||
|
||||
const loadDataMigrateHosts = async () => {
|
||||
if (!dataMigrateForm.target_service_id) return
|
||||
dataMigrateHostsLoading.value = true
|
||||
try {
|
||||
const res = await getRemoteHostList({ service_id: dataMigrateForm.target_service_id, page: 1, page_size: 10 })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
dataMigrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
|
||||
}
|
||||
} catch { /* */ } finally { dataMigrateHostsLoading.value = false }
|
||||
}
|
||||
|
||||
const submitDataMigrate = async () => {
|
||||
if (!dataMigrateForm.target_service_id) { ElMessage.warning('请选择目标主控服务'); return }
|
||||
if (!dataMigrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('source_service_id', serviceId.value)
|
||||
fd.append('source_vm_id', vmId.value)
|
||||
fd.append('target_service_id', dataMigrateForm.target_service_id)
|
||||
fd.append('target_host_id', dataMigrateForm.target_host_id)
|
||||
if (dataMigrateForm.ipv4_num) fd.append('ipv4_num', dataMigrateForm.ipv4_num)
|
||||
if (dataMigrateForm.ipv6_num) fd.append('ipv6_num', dataMigrateForm.ipv6_num)
|
||||
const res = await dataMigrateVm(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('数据迁移已发起')
|
||||
dataMigrationId.value = res.data.data?.migration_id || ''
|
||||
dataMigrateVisible.value = false
|
||||
// 自动打开进度弹窗
|
||||
dataMigrateProgressData.value = res.data.data
|
||||
dataMigrateProgressVisible.value = true
|
||||
} else ElMessage.error(extractApiError(res?.data, '发起迁移失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '发起迁移失败')) } finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
const loadDataMigrateProgress = async () => {
|
||||
if (!dataMigrationId.value) return
|
||||
dataMigrateProgressLoading.value = true
|
||||
try {
|
||||
const res = await getDataMigrateProgress({ migration_id: dataMigrationId.value, service_id: serviceId.value })
|
||||
if (res?.data?.code === 200) dataMigrateProgressData.value = res.data.data
|
||||
else ElMessage.warning('暂无进度信息')
|
||||
} catch { /* */ } finally { dataMigrateProgressLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- VNC 连接 ----
|
||||
const vncDialogVisible = ref(false)
|
||||
const vncNodeId = ref(null)
|
||||
@@ -2048,7 +2185,7 @@ const handleVolUnmount = (row) => {
|
||||
}
|
||||
const loadVmListOptions = async () => {
|
||||
try {
|
||||
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 200 })
|
||||
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
vmListOptions.value = inner.vms || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
@@ -2790,6 +2927,22 @@ const triggerTabLoad = (tab) => {
|
||||
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
|
||||
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
||||
if (tab === 'userNetworking') loadVmNetworkingList()
|
||||
if (tab === 'security') loadSgLockInfo()
|
||||
}
|
||||
|
||||
// 请求安全组详情补充 lock 字段
|
||||
const loadSgLockInfo = async () => {
|
||||
const groups = [vmPortGroup.value, vmOutPortGroup.value].filter(Boolean)
|
||||
for (const sg of groups) {
|
||||
try {
|
||||
const res = await getSecurityGroupDetail({ service_id: serviceId.value, id: sg.id })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data.group || res.data.data.data || res.data.data
|
||||
if (vmPortGroup.value?.id === sg.id) vmPortGroup.value = { ...vmPortGroup.value, lock: d.lock ?? sg.lock }
|
||||
if (vmOutPortGroup.value?.id === sg.id) vmOutPortGroup.value = { ...vmOutPortGroup.value, lock: d.lock ?? sg.lock }
|
||||
}
|
||||
} catch { /* */ }
|
||||
}
|
||||
}
|
||||
|
||||
watch(vmId, () => { if (isPageActive) initPage() })
|
||||
|
||||
Reference in New Issue
Block a user