feat: 添加用户虚拟机商品管理
Build and Deploy Vue3 / build (push) Successful in 1m40s
Build and Deploy Vue3 / deploy (push) Successful in 1m8s

This commit is contained in:
2026-03-31 15:13:04 +08:00
parent 71d3605f4f
commit c07e09c151
28 changed files with 7143 additions and 621 deletions
+90 -33
View File
@@ -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()