feat: 添加用户虚拟机商品管理
This commit is contained in:
@@ -161,7 +161,8 @@
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">网络列表</h3>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button size="small" type="primary" @click="showNetBindSelector = true">绑定网络</el-button>
|
||||
<el-button size="small" type="primary" @click="showNetBindBridgeSelector = true">绑定外网</el-button>
|
||||
<el-button size="small" type="success" @click="showNetBindNatSelector = true">绑定内网</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,7 +262,7 @@
|
||||
<el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="sgTableData" size="small" stripe>
|
||||
<el-table :data="pagedSecurityGroups" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="锁定" width="80">
|
||||
@@ -301,6 +302,20 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!sgTableData.length" description="暂无绑定的安全组" :image-size="60" />
|
||||
|
||||
<div class="pagination-wrapper" v-if="sgTableData.length > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="sgPage"
|
||||
v-model:page-size="sgPageSize"
|
||||
:page-size="[10,20,50]"
|
||||
:total="sgTableData.length"
|
||||
layout="total,sizes,prev,pager,next"
|
||||
small
|
||||
@size-change="s => {sgPageSize = s; sgPage = 1}"
|
||||
@current-change="p => {sgPage = p}"
|
||||
>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -826,8 +841,10 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 绑定网络选择器 -->
|
||||
<NetworkSelectorPopup v-model="showNetBindSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindConfirm" @create="() => handleNetCreate('bind')" />
|
||||
<!-- 绑定外网选择器(bridge) -->
|
||||
<NetworkSelectorPopup v-model="showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" />
|
||||
<!-- 绑定内网选择器(nat) -->
|
||||
<NetworkSelectorPopup v-model="showNetBindNatSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleNetBindNatConfirm" @create="() => handleNetCreate('bindNat')" />
|
||||
|
||||
<!-- 创建/编辑网络弹窗 -->
|
||||
<el-dialog v-model="netDialogVisible" :title="netDialogType === 'add' ? '创建网络' : '编辑网络'" width="600px" destroy-on-close>
|
||||
@@ -1249,8 +1266,8 @@ const handleMoreCommand = (cmd) => {
|
||||
if (actionMap[cmd]) actionMap[cmd]()
|
||||
}
|
||||
|
||||
const vmStatusType = (s) => ({ running: 'success', ready: 'success', creating: 'warning', pending: 'info', stopped: 'danger', stop: 'danger', error: 'danger', paused: 'warning', reboot: 'warning', poweroff: 'info', unknown: 'info' }[s] || 'info')
|
||||
const vmStatusLabel = (s) => ({ running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中', stopped: '已停止', stop: '已停止', error: '错误', paused: '已暂停', reboot: '重启中', poweroff: '已关机', unknown: '未知' }[s] || s || '-')
|
||||
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 imgStatusType = (s) => ({ ready: 'success', downloading: 'warning', pending: 'info', error: 'danger' }[s] || 'info')
|
||||
const imgStatusLabel = (s) => ({ ready: '就绪', downloading: '下载中', pending: '等待中', error: '错误' }[s] || s || '-')
|
||||
|
||||
@@ -1298,9 +1315,12 @@ const fetchVmStatus = async () => {
|
||||
try {
|
||||
const res = await getVmStatus({ service_id: serviceId.value, vm_id: vmId.value })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const sd = res.data.data
|
||||
detail.value = { ...detail.value, status: sd.status ?? sd }
|
||||
ElMessage.success('状态已刷新: ' + vmStatusLabel(detail.value.status))
|
||||
const outer = res.data.data
|
||||
const inner = outer.data ?? outer
|
||||
const state = inner.state ?? inner.status ?? inner
|
||||
const desc = inner.desc || ''
|
||||
detail.value = { ...detail.value, status: state }
|
||||
ElMessage.success(`状态已刷新: ${desc || vmStatusLabel(state)}`)
|
||||
}
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取状态失败')) } finally { statusLoading.value = false }
|
||||
}
|
||||
@@ -1476,7 +1496,11 @@ const submitRebuild = async () => {
|
||||
if (!rebuildImageId.value) { ElMessage.warning('请选择镜像'); return }
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const res = await rebuildVm({ service_id: serviceId.value, vm_id: vmId.value, image_id: rebuildImageId.value })
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('vm_id', vmId.value)
|
||||
fd.append('image_id', rebuildImageId.value)
|
||||
const res = await rebuildVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('重装成功'); rebuildDialogVisible.value = false; loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '重装失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重装失败')) } finally { actionLoading.value = false }
|
||||
@@ -1542,7 +1566,7 @@ const handleEditVm = async () => {
|
||||
Object.assign(editForm, {
|
||||
rx_bandwidth: d.rx_bandwidth || 0,
|
||||
tx_bandwidth: d.tx_bandwidth || 0,
|
||||
root_password: '',
|
||||
root_password: d.root_password || '',
|
||||
ssh_port: d.ssh_port || 22,
|
||||
port_group_id: vmPortGroup.value?.id || ''
|
||||
})
|
||||
@@ -1570,7 +1594,7 @@ const submitEditVm = async () => {
|
||||
if (editForm.root_password) fd.append('root_password', editForm.root_password)
|
||||
fd.append('ssh_port', editForm.ssh_port)
|
||||
editSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
|
||||
editSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.id))
|
||||
if (editSelectedInternalNetworks.value.length) fd.append('internet_network_id', editSelectedInternalNetworks.value[0].id)
|
||||
if (editForm.port_group_id) fd.append('port_group_id', editForm.port_group_id)
|
||||
const res = await updateVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('修改成功'); editDialogVisible.value = false; loadDetail() }
|
||||
@@ -1662,7 +1686,7 @@ const submitRefactorVm = async () => {
|
||||
if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port)
|
||||
if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password)
|
||||
refactorSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
|
||||
refactorSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.id))
|
||||
if (refactorSelectedInternalNetworks.value.length) fd.append('internet_network_id', refactorSelectedInternalNetworks.value[0].id)
|
||||
if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id)
|
||||
const res = await refactorVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() }
|
||||
@@ -1798,20 +1822,25 @@ const loadSgOptions = async () => {
|
||||
}
|
||||
|
||||
// ---- 绑定网络(通过 updateVm 接口) ----
|
||||
const showNetBindSelector = ref(false)
|
||||
const handleNetBindConfirm = async (selectedNetwork) => {
|
||||
const existingIds = vmNetworks.value.map(n => n.id)
|
||||
if (existingIds.includes(selectedNetwork.id)) {
|
||||
ElMessage.warning('该网络已绑定')
|
||||
return
|
||||
}
|
||||
const showNetBindBridgeSelector = ref(false)
|
||||
const showNetBindNatSelector = ref(false)
|
||||
|
||||
const submitNetBind = async (paramName, newId, existingIds) => {
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const allNetworkIds = [...existingIds, selectedNetwork.id]
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('vm_id', vmId.value)
|
||||
allNetworkIds.forEach(id => fd.append('network_ids', id))
|
||||
const bridgeIds = vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.id)
|
||||
const natNet = vmNetworks.value.find(n => n.type === 'nat')
|
||||
if (paramName === 'network_ids') {
|
||||
const allBridge = [...bridgeIds, newId]
|
||||
allBridge.forEach(id => fd.append('network_ids', id))
|
||||
if (natNet) fd.append('internet_network_id', natNet.id)
|
||||
} else {
|
||||
bridgeIds.forEach(id => fd.append('network_ids', id))
|
||||
fd.append('internet_network_id', newId)
|
||||
}
|
||||
if (detail.value?.rx_bandwidth) fd.append('rx_bandwidth', detail.value.rx_bandwidth)
|
||||
if (detail.value?.tx_bandwidth) fd.append('tx_bandwidth', detail.value.tx_bandwidth)
|
||||
if (detail.value?.ssh_port) fd.append('ssh_port', detail.value.ssh_port)
|
||||
@@ -1821,15 +1850,33 @@ const handleNetBindConfirm = async (selectedNetwork) => {
|
||||
ElMessage.success('绑定网络成功')
|
||||
loadDetail()
|
||||
} else {
|
||||
ElMessage.error(extractApiError(res, '绑定网络失败'))
|
||||
ElMessage.error(extractApiError(res?.data, '绑定网络失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e, '绑定网络失败'))
|
||||
ElMessage.error(extractApiError(e?.response?.data, '绑定网络失败'))
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleNetBindBridgeConfirm = (selectedNetwork) => {
|
||||
const existingIds = vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.id)
|
||||
if (existingIds.includes(selectedNetwork.id)) {
|
||||
ElMessage.warning('该网络已绑定')
|
||||
return
|
||||
}
|
||||
submitNetBind('network_ids', selectedNetwork.id)
|
||||
}
|
||||
|
||||
const handleNetBindNatConfirm = (selectedNetwork) => {
|
||||
const existingNat = vmNetworks.value.find(n => n.type === 'nat')
|
||||
if (existingNat?.id === selectedNetwork.id) {
|
||||
ElMessage.warning('该内网已绑定')
|
||||
return
|
||||
}
|
||||
submitNetBind('internet_network_id', selectedNetwork.id)
|
||||
}
|
||||
|
||||
// ---- 网络操作(创建/编辑/删除/详情) ----
|
||||
const netDialogVisible = ref(false)
|
||||
const netDialogType = ref('add')
|
||||
@@ -1859,7 +1906,8 @@ const handleNetDialogCancel = () => {
|
||||
else if (src === 'editInternal') showEditInternalNetworkSelector.value = true
|
||||
else if (src === 'refactor') showRefactorNetworkSelector.value = true
|
||||
else if (src === 'refactorInternal') showRefactorInternalNetworkSelector.value = true
|
||||
else if (src === 'bind') showNetBindSelector.value = true
|
||||
else if (src === 'bindBridge') showNetBindBridgeSelector.value = true
|
||||
else if (src === 'bindNat') showNetBindNatSelector.value = true
|
||||
}
|
||||
|
||||
const handleNetEdit = (row) => {
|
||||
@@ -2045,6 +2093,13 @@ const sgTableData = computed(() => {
|
||||
if (vmOutPortGroup.value) list.push(vmOutPortGroup.value)
|
||||
return list
|
||||
})
|
||||
//安全组分页
|
||||
const sgPage = ref(1)
|
||||
const sgPageSize = ref(10)
|
||||
const pagedSecurityGroups = computed(() =>{
|
||||
const start = (sgPage.value -1) * sgPageSize.value
|
||||
return sgTableData.value.slice(start,start + sgPageSize.value)
|
||||
})
|
||||
const sgSubmitLoading = ref(false)
|
||||
const sgDetailLoading = ref(false)
|
||||
const sgHostOptions = ref([])
|
||||
@@ -2609,7 +2664,6 @@ const vnCreateForm = reactive({ name: '', bridge_name: '', gateway: '' })
|
||||
const vnCreateHostName = ref('')
|
||||
const vnCreateUserName = ref('')
|
||||
const vnCreateRules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
bridge_name: [{ required: true, message: '请输入网桥名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
@@ -2717,7 +2771,7 @@ const handleLeaveNetworking = (row) => {
|
||||
}
|
||||
|
||||
let loadedVmId = null
|
||||
const initPage = () => {
|
||||
const initPage = async () => {
|
||||
if (!vmId.value || loadedVmId === vmId.value) return
|
||||
loadedVmId = vmId.value
|
||||
metricsData.value = null
|
||||
@@ -2725,18 +2779,21 @@ const initPage = () => {
|
||||
disposeCharts()
|
||||
clearHistory()
|
||||
loadHostOptions()
|
||||
loadDetail()
|
||||
if (activeTab.value === 'monitor') startPolling()
|
||||
// 先加载详情,详情加载完后再触发当前 tab 的数据
|
||||
await loadDetail()
|
||||
triggerTabLoad(activeTab.value)
|
||||
}
|
||||
|
||||
watch(vmId, () => { if (isPageActive) initPage() })
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'monitor' && detail.value) startPolling()
|
||||
const triggerTabLoad = (tab) => {
|
||||
if (tab === 'monitor') startPolling()
|
||||
else stopPolling()
|
||||
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
|
||||
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
||||
if (tab === 'userNetworking') loadVmNetworkingList()
|
||||
})
|
||||
}
|
||||
|
||||
watch(vmId, () => { if (isPageActive) initPage() })
|
||||
watch(activeTab, (tab) => { if (detail.value) triggerTabLoad(tab) })
|
||||
onActivated(() => {
|
||||
isPageActive = true
|
||||
if (loadedVmId !== vmId.value) initPage()
|
||||
|
||||
Reference in New Issue
Block a user