fix: 重构虚拟机内网外网参数设置选择网络
This commit is contained in:
@@ -260,6 +260,13 @@ export const updateNetwork = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量创建网络 */
|
||||
export const batchCreateNetwork = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/network/batch_create', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除网络 */
|
||||
export const deleteNetwork = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/network/delete', { params })
|
||||
@@ -436,6 +443,13 @@ export const deleteVm = (params) => {
|
||||
return http2.delete('/api/v1/admin/server/host_service/point/vm/delete', { params })
|
||||
}
|
||||
|
||||
/** 迁移虚拟机(更换宿主机) */
|
||||
export const migrateVm = (data) => {
|
||||
return http2.post('/api/v1/admin/service/host_service/point/vm/migrate', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 安全组管理
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="typeFilter" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-select v-if="!filterType" v-model="typeFilter" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="网桥(Bridge)" value="bridge" />
|
||||
<el-option label="内网(NAT)" value="nat" />
|
||||
</el-select>
|
||||
<el-tag v-else type="success" size="small">仅{{ filterType === 'bridge' ? '网桥' : filterType === 'nat' ? '内网' : filterType }}</el-tag>
|
||||
<el-select v-if="!filterUsed" v-model="usedFilter" placeholder="占用状态" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="未占用" value="false" />
|
||||
<el-option label="已占用" value="true" />
|
||||
</el-select>
|
||||
<el-tag v-else :type="filterUsed === 'false' ? 'success' : 'info'" size="small">{{ filterUsed === 'false' ? '仅未占用' : '仅已占用' }}</el-tag>
|
||||
<el-select v-model="ipVersionFilter" placeholder="IP版本" clearable style="width: 110px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadList" circle />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||
@@ -26,6 +36,11 @@
|
||||
<el-table-column prop="gateway" label="网关" width="130" />
|
||||
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.used ? 'danger' : 'success'" size="small">{{ row.used ? '已占用' : '空闲' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
@@ -54,7 +69,9 @@ import { getNetworkList } from '@/api/admin/kvmService'
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
serviceId: { type: Number, default: 0 },
|
||||
hostId: { type: Number, default: 0 }
|
||||
hostId: { type: Number, default: 0 },
|
||||
filterType: { type: String, default: '' },
|
||||
filterUsed: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'create'])
|
||||
@@ -67,6 +84,8 @@ const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const typeFilter = ref('')
|
||||
const usedFilter = ref('')
|
||||
const ipVersionFilter = ref('')
|
||||
const selectedItem = ref(null)
|
||||
const type = ref('bridge')
|
||||
|
||||
@@ -75,7 +94,9 @@ watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
page.value = 1
|
||||
keyword.value = ''
|
||||
typeFilter.value = ''
|
||||
typeFilter.value = props.filterType || ''
|
||||
usedFilter.value = props.filterUsed || ''
|
||||
ipVersionFilter.value = ''
|
||||
selectedItem.value = null
|
||||
loadList()
|
||||
}
|
||||
@@ -88,9 +109,13 @@ const loadList = async () => {
|
||||
if (!props.serviceId || !props.hostId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value,type: type.value }
|
||||
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value }
|
||||
const effectiveType = props.filterType || typeFilter.value || type.value
|
||||
if (effectiveType) params.type = effectiveType
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
if (typeFilter.value) params.type = typeFilter.value
|
||||
const effectiveUsed = props.filterUsed || usedFilter.value
|
||||
if (effectiveUsed) params.used = effectiveUsed
|
||||
if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value
|
||||
const res = await getNetworkList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ const isDevelopment = import.meta.env.MODE === 'development'
|
||||
// API 基础地址
|
||||
// 开发环境使用 vite 代理 (baseUrl 为空),生产环境使用实际地址
|
||||
const API_BASE_MAP = {
|
||||
development: '', // 开发环境通过 vite proxy 代理
|
||||
development: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren', // 直接请求后端,不走 vite proxy
|
||||
production: import.meta.env.VITE_API_BASE_URL || 'https://cloudapi.007yjs.com',
|
||||
staging: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren'
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const currentEnv = import.meta.env.VITE_APP_ENV || import.meta.env.MODE || 'deve
|
||||
export const baseUrl = API_BASE_MAP[currentEnv] || API_BASE_MAP.development
|
||||
|
||||
// ACS 服务基础地址
|
||||
export const acsBaseUrl = isDevelopment ? '' : baseUrl
|
||||
export const acsBaseUrl = baseUrl
|
||||
|
||||
// 网站标题
|
||||
export const siteTitle = '007UI管理系统'
|
||||
|
||||
@@ -201,7 +201,136 @@
|
||||
<el-tab-pane label="备份管理" name="backup">
|
||||
<BackupManage v-if="hostTabLoaded['backup']" ref="backupManageRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="组网管理" name="networking">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">用户组网列表</h3>
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<el-input v-model="nwFilterUserId" placeholder="按用户ID筛选" style="width: 140px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
|
||||
<el-input v-model="nwKeyword" placeholder="关键词搜索" style="width: 160px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
|
||||
<el-button size="small" :icon="Search" @click="loadNetworkingList">搜索</el-button>
|
||||
<el-button size="small" type="primary" @click="handleNwCreate">创建组网</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="loadNetworkingList" :loading="nwLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="nwList" v-loading="nwLoading" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||
<el-table-column prop="host_id" label="宿主机ID" width="90" />
|
||||
<el-table-column label="网桥" width="120">
|
||||
<template #default="{ row }">{{ row.bridge_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="网关" min-width="140">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.gateway || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleNwDetail(row)">详情</el-button>
|
||||
<el-button link type="success" size="small" @click="handleNwAssign(row)">分配IP</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleNwDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!nwList.length && !nwLoading" description="暂无组网" :image-size="60" />
|
||||
<div class="pagination-wrapper" v-if="nwTotal > 0">
|
||||
<el-pagination v-model:current-page="nwPage" v-model:page-size="nwPageSize"
|
||||
:page-sizes="[10, 20, 50]" :total="nwTotal" layout="total, sizes, prev, pager, next" small
|
||||
@size-change="s => { nwPageSize = s; nwPage = 1; loadNetworkingList() }"
|
||||
@current-change="p => { nwPage = p; loadNetworkingList() }" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 组网详情弹窗 -->
|
||||
<el-dialog v-model="nwDetailVisible" title="组网详情" width="800px" destroy-on-close>
|
||||
<el-descriptions :column="2" border v-if="nwDetailData" style="margin-bottom: 16px">
|
||||
<el-descriptions-item label="ID">{{ nwDetailData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ nwDetailData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ nwDetailData.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="宿主机ID">{{ nwDetailData.host_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网桥">{{ nwDetailData.bridge_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网关">{{ nwDetailData.gateway || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTimestamp(nwDetailData.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatTimestamp(nwDetailData.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<h4 style="margin: 0 0 12px">组网下的网络 (已分配)</h4>
|
||||
<el-table :data="nwDetailNetworks" size="small" stripe>
|
||||
<el-table-column label="网络ID" width="70">
|
||||
<template #default="{ row }">{{ row.network?.id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP地址" min-width="150">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.network?.address || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="MAC地址" min-width="160">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.network?.mac_address || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }"><el-tag size="small">{{ row.network?.type || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="虚拟机ID" width="90">
|
||||
<template #default="{ row }">{{ row.network?.vm_id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" size="small" @click="handleNwRemoveNet(row)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!nwDetailNetworks.length" description="暂无分配的网络" :image-size="50" />
|
||||
<template #footer><el-button @click="nwDetailVisible = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 创建组网弹窗 -->
|
||||
<el-dialog v-model="nwCreateVisible" title="创建组网" width="480px" destroy-on-close>
|
||||
<el-form ref="nwCreateFormRef" :model="nwCreateForm" :rules="nwCreateRules" label-width="100px">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwCreateForm.user_id ? `${nwCreateUserName} (ID: ${nwCreateForm.user_id})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwUserSelector = true">选择</el-button>
|
||||
<el-button v-if="nwCreateForm.user_id" @click="nwCreateForm.user_id = 0; nwCreateUserName = ''">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网桥名称">
|
||||
<el-input v-model="nwCreateForm.bridge_name" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关">
|
||||
<el-input v-model="nwCreateForm.gateway" placeholder="可选,如 10.0.0.1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nwCreateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配IP弹窗 -->
|
||||
<el-dialog v-model="nwAssignVisible" title="为虚拟机分配组网IP" width="480px" destroy-on-close>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="组网">{{ nwAssignTarget?.name || '-' }} (ID: {{ nwAssignTarget?.id }})</el-form-item>
|
||||
<el-form-item label="虚拟机" required>
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwAssignVmId ? `${nwAssignVmName} (ID: ${nwAssignVmId})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwVmSelector = true">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定IP">
|
||||
<el-input v-model="nwAssignIp" placeholder="留空自动分配" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nwAssignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwAssign" :disabled="!nwAssignVmId">分配</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<UserListSelector v-model="showNwUserSelector" @confirm="handleNwUserSelected" />
|
||||
<VmSelectorPopup v-model="showNwVmSelector" :service-id="serviceId" :host-id="hostId" @confirm="handleNwVmSelected" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
@@ -267,9 +396,11 @@
|
||||
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Box, Connection } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Box, Connection, Search, Plus } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost
|
||||
getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost,
|
||||
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
|
||||
assignUserNetworking, removeUserNetworkingNetwork
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||||
@@ -280,6 +411,8 @@ import VmManage from '@/views/virtualization/VmManage.vue'
|
||||
import SnapshotManage from '@/views/virtualization/SnapshotManage.vue'
|
||||
import BackupManage from '@/views/virtualization/BackupManage.vue'
|
||||
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -291,7 +424,7 @@ const serviceName = computed(() => route.query.service_name || '')
|
||||
const hostId = computed(() => parseInt(route.query.id) || 0)
|
||||
|
||||
const activeTab = ref('info')
|
||||
const hostTabLoaded = reactive({ image: false, network: false, volume: false, vm: false, snapshot: false, backup: false })
|
||||
const hostTabLoaded = reactive({ image: false, network: false, volume: false, vm: false, snapshot: false, backup: false, networking: false })
|
||||
|
||||
const imageManageRef = ref(null)
|
||||
const networkManageRef = ref(null)
|
||||
@@ -302,7 +435,7 @@ const backupManageRef = ref(null)
|
||||
const tabRefMap = { image: imageManageRef, network: networkManageRef, volume: volumeManageRef, vm: vmManageRef, snapshot: snapshotManageRef, backup: backupManageRef }
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (!['info', 'monitor'].includes(tab)) {
|
||||
if (!['info', 'monitor', 'networking'].includes(tab)) {
|
||||
if (!hostTabLoaded[tab]) {
|
||||
hostTabLoaded[tab] = true
|
||||
} else {
|
||||
@@ -311,6 +444,7 @@ watch(activeTab, (tab) => {
|
||||
}
|
||||
if (tab === 'monitor' && detail.value) { loadMetrics(); startPolling() }
|
||||
else stopPolling()
|
||||
if (tab === 'networking') loadNetworkingList()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -607,6 +741,170 @@ const goBack = () => {
|
||||
router.push({ path: '/virtualization/kvm-service-detail', query: { service_id: serviceId.value, service_name: serviceName.value } })
|
||||
}
|
||||
|
||||
// ---- 组网管理 ----
|
||||
const nwLoading = ref(false)
|
||||
const nwSubmitLoading = ref(false)
|
||||
const nwList = ref([])
|
||||
const nwTotal = ref(0)
|
||||
const nwPage = ref(1)
|
||||
const nwPageSize = ref(10)
|
||||
const nwFilterUserId = ref('')
|
||||
const nwKeyword = ref('')
|
||||
|
||||
const nwDetailVisible = ref(false)
|
||||
const nwDetailData = ref(null)
|
||||
const nwDetailNetworks = ref([])
|
||||
const nwDetailLoading = ref(false)
|
||||
|
||||
const nwCreateVisible = ref(false)
|
||||
const nwCreateFormRef = ref(null)
|
||||
const nwCreateForm = reactive({ user_id: 0, bridge_name: '', gateway: '' })
|
||||
const nwCreateUserName = ref('')
|
||||
const showNwUserSelector = ref(false)
|
||||
const nwCreateRules = {
|
||||
user_id: [{ required: true, message: '请选择用户', trigger: 'change', type: 'number', min: 1 }]
|
||||
}
|
||||
|
||||
const nwAssignVisible = ref(false)
|
||||
const nwAssignTarget = ref(null)
|
||||
const nwAssignVmId = ref(0)
|
||||
const nwAssignVmName = ref('')
|
||||
const nwAssignIp = ref('')
|
||||
const showNwVmSelector = ref(false)
|
||||
|
||||
const loadNetworkingList = async () => {
|
||||
nwLoading.value = true
|
||||
try {
|
||||
const params = {
|
||||
service_id: serviceId.value,
|
||||
page: nwPage.value,
|
||||
count: nwPageSize.value,
|
||||
host_id: hostId.value
|
||||
}
|
||||
if (nwFilterUserId.value) params.user_id = parseInt(nwFilterUserId.value)
|
||||
if (nwKeyword.value) params.keyword = nwKeyword.value
|
||||
const res = await getUserNetworkingList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
nwList.value = Array.isArray(inner) ? inner : (inner.data || [])
|
||||
nwTotal.value = inner.meta?.count ?? inner.total ?? nwList.value.length
|
||||
}
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网列表失败')) }
|
||||
finally { nwLoading.value = false }
|
||||
}
|
||||
|
||||
const handleNwDetail = async (row) => {
|
||||
nwDetailVisible.value = true
|
||||
nwDetailData.value = row
|
||||
nwDetailNetworks.value = []
|
||||
nwDetailLoading.value = true
|
||||
try {
|
||||
const res = await getUserNetworkingDetail({ service_id: serviceId.value, networking_id: row.id })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
nwDetailData.value = inner.data ?? inner
|
||||
nwDetailNetworks.value = inner.networks || []
|
||||
}
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取详情失败')) }
|
||||
finally { nwDetailLoading.value = false }
|
||||
}
|
||||
|
||||
const handleNwCreate = () => {
|
||||
Object.assign(nwCreateForm, { user_id: 0, bridge_name: '', gateway: '' })
|
||||
nwCreateUserName.value = ''
|
||||
nwCreateVisible.value = true
|
||||
}
|
||||
|
||||
const handleNwUserSelected = (user) => {
|
||||
nwCreateForm.user_id = user.user_id || user.id
|
||||
nwCreateUserName.value = user.user_name || user.name || ''
|
||||
}
|
||||
|
||||
const submitNwCreate = async () => {
|
||||
if (!nwCreateForm.user_id) { ElMessage.warning('请选择用户'); return }
|
||||
nwSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('host_id', hostId.value)
|
||||
fd.append('user_id', nwCreateForm.user_id)
|
||||
if (nwCreateForm.bridge_name) fd.append('bridge_name', nwCreateForm.bridge_name)
|
||||
if (nwCreateForm.gateway) fd.append('gateway', nwCreateForm.gateway)
|
||||
const res = await createUserNetworking(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
nwCreateVisible.value = false
|
||||
loadNetworkingList()
|
||||
} else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
|
||||
finally { nwSubmitLoading.value = false }
|
||||
}
|
||||
|
||||
const handleNwDelete = (row) => {
|
||||
ElMessageBox.confirm(`确定删除组网「${row.name || row.id}」?该操作不可撤销。`, '删除确认', { type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await deleteUserNetworking({ service_id: serviceId.value, networking_id: row.id })
|
||||
if (res?.data?.code === 200) { ElMessage.success('已删除'); loadNetworkingList() }
|
||||
else ElMessage.error(extractApiError(res?.data, '删除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleNwAssign = (row) => {
|
||||
nwAssignTarget.value = row
|
||||
nwAssignVmId.value = 0
|
||||
nwAssignVmName.value = ''
|
||||
nwAssignIp.value = ''
|
||||
nwAssignVisible.value = true
|
||||
}
|
||||
|
||||
const handleNwVmSelected = (vm) => {
|
||||
nwAssignVmId.value = vm.id
|
||||
nwAssignVmName.value = vm.name || ''
|
||||
}
|
||||
|
||||
const submitNwAssign = async () => {
|
||||
if (!nwAssignVmId.value || !nwAssignTarget.value) { ElMessage.warning('请选择虚拟机'); return }
|
||||
nwSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('networking_id', nwAssignTarget.value.id)
|
||||
fd.append('vm_id', nwAssignVmId.value)
|
||||
if (nwAssignIp.value.trim()) fd.append('ip', nwAssignIp.value.trim())
|
||||
const res = await assignUserNetworking(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('分配成功')
|
||||
nwAssignVisible.value = false
|
||||
if (nwDetailVisible.value && nwDetailData.value?.id === nwAssignTarget.value.id) {
|
||||
handleNwDetail(nwAssignTarget.value)
|
||||
}
|
||||
} else ElMessage.error(extractApiError(res?.data, '分配失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '分配失败')) }
|
||||
finally { nwSubmitLoading.value = false }
|
||||
}
|
||||
|
||||
const handleNwRemoveNet = (netItem) => {
|
||||
const net = netItem.network || netItem
|
||||
if (!net.id || !nwDetailData.value?.id) { ElMessage.warning('缺少网络信息'); return }
|
||||
ElMessageBox.confirm(`确定移除网络 (ID: ${net.id}, VM: ${net.vm_id || '-'}) ?`, '移除确认', { type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('networking_id', nwDetailData.value.id)
|
||||
fd.append('network_id', net.id)
|
||||
fd.append('vm_id', net.vm_id || 0)
|
||||
const res = await removeUserNetworkingNetwork(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('已移除')
|
||||
handleNwDetail(nwDetailData.value)
|
||||
} else ElMessage.error(extractApiError(res?.data, '移除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
let loadedHostId = null
|
||||
|
||||
const initPage = () => {
|
||||
@@ -694,4 +992,5 @@ onBeforeUnmount(() => { isPageActive = false; stopPolling(); disposeCharts() })
|
||||
|
||||
.unit-input-row { display: flex; gap: 6px; width: 100%; }
|
||||
.wide-number { flex: 1; min-width: 140px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
</style>
|
||||
|
||||
@@ -119,8 +119,8 @@
|
||||
<el-form-item label="SSH 密码">
|
||||
<el-input v-model="formData.password" placeholder="SSH 密码(可选)" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="私钥路径">
|
||||
<el-input v-model="formData.private_key_path" placeholder="SSH 私钥文件路径(可选)" />
|
||||
<el-form-item label="SSH 私钥">
|
||||
<el-input v-model="formData.private_key" type="textarea" :rows="4" placeholder="SSH 私钥内容(可选)" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">资源限制</el-divider>
|
||||
<el-form-item label="最大CPU(核)">
|
||||
@@ -206,7 +206,7 @@
|
||||
<el-input v-if="currentDetail.password" :model-value="currentDetail.password" readonly show-password style="max-width: 200px" />
|
||||
<span v-else class="text-muted">未设置</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="私钥路径">{{ currentDetail.private_key_path || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="SSH 私钥">{{ currentDetail.private_key ? '已配置' : '未设置' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大CPU">{{ currentDetail.max_cpu ? currentDetail.max_cpu + ' 核' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大内存">{{ formatMemKB(currentDetail.max_memory) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大磁盘">{{ formatDiskGB(currentDetail.max_disk) }}</el-descriptions-item>
|
||||
@@ -324,7 +324,7 @@ const metricsData = ref(null)
|
||||
|
||||
const formData = reactive({
|
||||
id: undefined, name: '', base_url: '', ip: '', token: '',
|
||||
port: 22, user: '', password: '', private_key_path: '',
|
||||
port: 22, user: '', password: '', private_key: '',
|
||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '',
|
||||
_groupName: ''
|
||||
@@ -408,7 +408,7 @@ const getGroupName = (gid) => {
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: undefined, name: '', base_url: '', ip: '', token: '',
|
||||
port: 22, user: '', password: '', private_key_path: '',
|
||||
port: 22, user: '', password: '', private_key: '',
|
||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '',
|
||||
_groupName: ''
|
||||
@@ -470,7 +470,7 @@ const handleEdit = (row) => {
|
||||
// 先用列表数据回填,密码需从详情取
|
||||
Object.assign(formData, {
|
||||
id: row.id, name: row.name, base_url: row.base_url, ip: row.ip, token: row.token || '',
|
||||
port: row.port || 22, user: row.user || '', password: row.password || '', private_key_path: row.private_key_path || '',
|
||||
port: row.port || 22, user: row.user || '', password: row.password || '', private_key: row.private_key || '',
|
||||
max_cpu: row.max_cpu || 0, max_memory: row.max_memory || 0, max_disk: row.max_disk || 0,
|
||||
rx_bandwidth: row.rx_bandwidth || 0, tx_bandwidth: row.tx_bandwidth || 0,
|
||||
host_group_id: row.host_group_id || 0, description: row.description || '',
|
||||
@@ -483,7 +483,7 @@ const handleEdit = (row) => {
|
||||
const detail = body.data.host ?? body.data.data ?? body.data
|
||||
if (detail.password) formData.password = detail.password
|
||||
if (detail.token) formData.token = detail.token
|
||||
if (detail.private_key_path) formData.private_key_path = detail.private_key_path
|
||||
if (detail.private_key) formData.private_key = detail.private_key
|
||||
}
|
||||
}).catch(() => {})
|
||||
dialogVisible.value = true
|
||||
@@ -500,7 +500,7 @@ const handleSubmit = () => {
|
||||
// 可选参数为空时不提交
|
||||
if (!payload.token) delete payload.token
|
||||
if (!payload.password) delete payload.password
|
||||
if (!payload.private_key_path) delete payload.private_key_path
|
||||
if (!payload.private_key) delete payload.private_key
|
||||
if (!payload.description) delete payload.description
|
||||
if (!payload.host_group_id) delete payload.host_group_id
|
||||
let res
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row._isHost" :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '启用' : '禁用' }}</el-tag>
|
||||
<el-tag v-if="row._isHost" :type="row.is_active ? 'success' : 'info'" size="small">{{ row.is_active ? '在线' : '离线' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
@@ -190,6 +190,9 @@
|
||||
<el-form-item label="SSH 密码">
|
||||
<el-input v-model="hostForm.password" placeholder="可选" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH 私钥">
|
||||
<el-input v-model="hostForm.private_key" type="textarea" :rows="4" placeholder="SSH 私钥内容(可选)" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">资源限制</el-divider>
|
||||
<el-form-item label="最大CPU(核)">
|
||||
<el-input-number v-model="hostForm.max_cpu" :min="0" controls-position="right" style="width: 240px" />
|
||||
@@ -525,7 +528,7 @@ const hostDialogType = ref('add')
|
||||
const hostFormRef = ref(null)
|
||||
const hostForm = reactive({
|
||||
id: undefined, name: '', base_url: '', ip: '', token: '',
|
||||
port: 22, user: '', password: '',
|
||||
port: 22, user: '', password: '', private_key: '',
|
||||
max_cpu: 0, max_memory: 0, max_disk: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: ''
|
||||
})
|
||||
@@ -537,13 +540,13 @@ const hostFormRules = {
|
||||
|
||||
const handleAddHost = () => {
|
||||
hostDialogType.value = 'add'
|
||||
Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '' })
|
||||
Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, description: '' })
|
||||
hostDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleAddHostToGroup = (group) => {
|
||||
hostDialogType.value = 'add'
|
||||
Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: group.id, description: '' })
|
||||
Object.assign(hostForm, { id: undefined, name: '', base_url: '', ip: '', token: '', port: 22, user: '', password: '', private_key: '', max_cpu: 0, max_memory: 0, max_disk: 0, rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: group.id, description: '' })
|
||||
hostDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -551,7 +554,7 @@ const handleEditHost = (row) => {
|
||||
hostDialogType.value = 'edit'
|
||||
Object.assign(hostForm, {
|
||||
id: row.id, name: row.name, base_url: row.base_url || '', ip: row.ip || '', token: row.token || '',
|
||||
port: row.port || 22, user: row.user || '', password: row.password || '',
|
||||
port: row.port || 22, user: row.user || '', password: row.password || '', private_key: row.private_key || '',
|
||||
max_cpu: row.max_cpu || 0, max_memory: row.max_memory || 0, max_disk: row.max_disk || 0,
|
||||
rx_bandwidth: row.rx_bandwidth || 0, tx_bandwidth: row.tx_bandwidth || 0,
|
||||
host_group_id: row.host_group_id || 0, description: row.description || ''
|
||||
@@ -561,6 +564,7 @@ const handleEditHost = (row) => {
|
||||
const d = res.data.data.host ?? res.data.data.data ?? res.data.data
|
||||
if (d.password) hostForm.password = d.password
|
||||
if (d.token) hostForm.token = d.token
|
||||
if (d.private_key) hostForm.private_key = d.private_key
|
||||
}
|
||||
}).catch(() => {})
|
||||
hostDialogVisible.value = true
|
||||
@@ -575,6 +579,7 @@ const submitHostForm = () => {
|
||||
delete payload.id
|
||||
if (!payload.token) delete payload.token
|
||||
if (!payload.password) delete payload.password
|
||||
if (!payload.private_key) delete payload.private_key
|
||||
if (!payload.host_group_id) delete payload.host_group_id
|
||||
if (!payload.description) delete payload.description
|
||||
let res
|
||||
|
||||
@@ -45,9 +45,17 @@
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">认证Token</div>
|
||||
<div class="stat-value">
|
||||
<el-tag v-if="serviceInfo.Token || serviceInfo.token" type="success" size="small">已设置</el-tag>
|
||||
<el-tag v-else type="info" size="small">未设置</el-tag>
|
||||
<div class="stat-value" v-if="serviceInfo.Token || serviceInfo.token">
|
||||
<code class="token-text">{{ tokenVisible ? (serviceInfo.Token || serviceInfo.token) : '••••••••••••' }}</code>
|
||||
<el-button link size="small" @click="tokenVisible = !tokenVisible" style="margin-left: 4px">
|
||||
<el-icon><View v-if="!tokenVisible" /><Hide v-else /></el-icon>
|
||||
</el-button>
|
||||
<el-button link size="small" @click="copyToken" style="margin-left: 2px">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="stat-value" v-else>
|
||||
<el-tag type="info" size="small">未设置</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
@@ -134,7 +142,7 @@
|
||||
import { ref, reactive, computed, provide, onMounted, onActivated, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor, View, Hide, CopyDocument } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getKvmServiceDetail, updateKvmService, deleteKvmService
|
||||
} from '@/api/admin/kvmService'
|
||||
@@ -241,6 +249,29 @@ const goBack = () => {
|
||||
router.push('/virtualization/kvm-service')
|
||||
}
|
||||
|
||||
// Token 显示/复制
|
||||
const tokenVisible = ref(false)
|
||||
const copyToken = () => {
|
||||
const token = serviceInfo.value.Token || serviceInfo.value.token
|
||||
if (!token) return
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = token
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
ElMessage.success('Token 已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
} finally {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 编辑服务
|
||||
const editDialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
@@ -482,6 +513,23 @@ onMounted(() => {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.token-text {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
user-select: all;
|
||||
word-break: break-all;
|
||||
max-width: 180px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-value {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>创建网络</el-button>
|
||||
<el-button type="success" @click="handleBatchAdd">批量创建</el-button>
|
||||
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="embedded-toolbar" v-if="embedded">
|
||||
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>创建网络</el-button>
|
||||
<el-button type="success" @click="handleBatchAdd">批量创建</el-button>
|
||||
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +29,10 @@
|
||||
<el-option label="网桥(Bridge)" value="bridge" />
|
||||
<el-option label="内网(NAT)" value="nat" />
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="filterIpVersion" placeholder="IP版本" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="IPv4" value="ipv4" />
|
||||
<el-option label="IPv6" value="ipv6" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 网络列表 -->
|
||||
@@ -133,6 +138,46 @@
|
||||
</el-descriptions>
|
||||
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量创建弹窗 -->
|
||||
<el-dialog v-model="batchDialogVisible" title="批量创建网络" width="560px" destroy-on-close>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 16px">通过指定 IP 范围(start_ip ~ end_ip)批量创建网络条目</el-alert>
|
||||
<el-form ref="batchFormRef" :model="batchForm" :rules="batchFormRules" label-width="120px">
|
||||
<el-form-item label="宿主机" prop="host_id">
|
||||
<el-select v-model="batchForm.host_id" placeholder="选择宿主机" filterable style="width: 100%">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="起始IP" prop="start_ip">
|
||||
<el-input v-model="batchForm.start_ip" placeholder="如 192.168.1.10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束IP" prop="end_ip">
|
||||
<el-input v-model="batchForm.end_ip" placeholder="如 192.168.1.50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关">
|
||||
<el-input v-model="batchForm.gateway" placeholder="可选,如 192.168.1.1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="子网掩码">
|
||||
<el-input v-model="batchForm.mask" placeholder="可选,如 24" />
|
||||
</el-form-item>
|
||||
<el-form-item label="DNS">
|
||||
<el-input v-model="batchForm.nameservers" placeholder="可选,如 114.114.114.114,8.8.8.8" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网桥名称">
|
||||
<el-input v-model="batchForm.bridge_name" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网络类型">
|
||||
<el-select v-model="batchForm.type" style="width: 100%">
|
||||
<el-option label="网桥(Bridge)" value="bridge" />
|
||||
<el-option label="内网(NAT)" value="nat" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleBatchSubmit">确定创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -141,7 +186,7 @@ import { ref, reactive, computed, inject, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { getRemoteHostList, getNetworkList, getNetworkDetail, createNetwork, updateNetwork, deleteNetwork } from '@/api/admin/kvmService'
|
||||
import { getRemoteHostList, getNetworkList, getNetworkDetail, createNetwork, batchCreateNetwork, updateNetwork, deleteNetwork } from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -160,6 +205,7 @@ const networkList = ref([])
|
||||
const total = ref(0)
|
||||
const keyword = ref('')
|
||||
const filterType = ref('')
|
||||
const filterIpVersion = ref('')
|
||||
const hostIdInput = ref(0)
|
||||
const hostOptions = ref([])
|
||||
const queryParams = reactive({ page: 1, page_size: 10 })
|
||||
@@ -213,6 +259,7 @@ const loadList = async () => {
|
||||
const params = { service_id: serviceId.value, host_id: hid, page: queryParams.page, page_size: queryParams.page_size }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (filterType.value) params.type = filterType.value
|
||||
if (filterIpVersion.value) params.ip_version = filterIpVersion.value
|
||||
const res = await getNetworkList(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
@@ -269,7 +316,7 @@ const handleSubmit = () => {
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createNetwork(fd)
|
||||
} else {
|
||||
fd.append('network_id', formData.id)
|
||||
fd.append('id', formData.id)
|
||||
res = await updateNetwork(fd)
|
||||
}
|
||||
if (res?.data?.code === 200) {
|
||||
@@ -309,6 +356,57 @@ const handleDelete = (row) => {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// ---- 批量创建网络 ----
|
||||
const batchDialogVisible = ref(false)
|
||||
const batchFormRef = ref(null)
|
||||
const batchForm = reactive({
|
||||
host_id: 0, start_ip: '', end_ip: '', gateway: '', mask: '',
|
||||
nameservers: '', bridge_name: '', type: 'bridge'
|
||||
})
|
||||
const batchFormRules = {
|
||||
host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }],
|
||||
start_ip: [{ required: true, message: '请输入起始IP', trigger: 'blur' }],
|
||||
end_ip: [{ required: true, message: '请输入结束IP', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleBatchAdd = () => {
|
||||
Object.assign(batchForm, {
|
||||
host_id: hostIdInput.value || hostId.value || 0,
|
||||
start_ip: '', end_ip: '', gateway: '', mask: '',
|
||||
nameservers: '', bridge_name: '', type: 'bridge'
|
||||
})
|
||||
batchDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleBatchSubmit = () => {
|
||||
batchFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('host_id', batchForm.host_id)
|
||||
fd.append('start_ip', batchForm.start_ip)
|
||||
fd.append('end_ip', batchForm.end_ip)
|
||||
if (batchForm.gateway) fd.append('gateway', batchForm.gateway)
|
||||
if (batchForm.mask) fd.append('mask', batchForm.mask)
|
||||
if (batchForm.nameservers) fd.append('nameservers', batchForm.nameservers)
|
||||
if (batchForm.bridge_name) fd.append('bridge_name', batchForm.bridge_name)
|
||||
if (batchForm.type) fd.append('type', batchForm.type)
|
||||
const res = await batchCreateNetwork(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('批量创建成功')
|
||||
batchDialogVisible.value = false
|
||||
loadList()
|
||||
} else {
|
||||
ElMessage.error(extractApiError(res?.data, '批量创建失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '批量创建失败'))
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => { router.push('/virtualization/kvm-service') }
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<el-table-column prop="port_range" label="端口范围" min-width="120" />
|
||||
<el-table-column prop="ip_range" label="IP 范围" min-width="140" />
|
||||
<el-table-column prop="priority" label="优先级" width="80" />
|
||||
<el-table-column prop="description" label="备注" width="80" />
|
||||
<el-table-column label="操作" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleEditRule(row)">编辑</el-button>
|
||||
@@ -162,6 +163,7 @@
|
||||
<el-form-item label="端口范围"><el-input v-model="ruleForm.port_range" placeholder="如 80 或 80-90" /></el-form-item>
|
||||
<el-form-item label="IP 范围"><el-input v-model="ruleForm.ip_range" placeholder="如 0.0.0.0/0" /></el-form-item>
|
||||
<el-form-item label="优先级"><el-input-number v-model="ruleForm.priority" :min="0" :max="9999" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="ruleForm.description" placeholder="请输入备注" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="ruleDialogVisible = false">取消</el-button>
|
||||
@@ -193,7 +195,7 @@ const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const serviceId = computed(() => parseInt(route.query.service_id) || 0)
|
||||
const serviceName = computed(() => route.query.service_name || '')
|
||||
const sgId = computed(() => parseInt(route.query.id) || 0)
|
||||
const sgId = computed(() => parseInt(route.query.sg_id) || 0)
|
||||
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
@@ -370,12 +372,12 @@ const handleSetShared = (shared) => {
|
||||
|
||||
const handleAddRule = () => {
|
||||
ruleDialogType.value = 'add'
|
||||
Object.assign(ruleForm, { id: undefined, group_id: sgId.value, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0 })
|
||||
Object.assign(ruleForm, { id: undefined, group_id: sgId.value, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0, description: '' })
|
||||
ruleDialogVisible.value = true
|
||||
}
|
||||
const handleEditRule = (rule) => {
|
||||
ruleDialogType.value = 'edit'
|
||||
Object.assign(ruleForm, { id: rule.id, group_id: sgId.value, port_group_id: sgId.value, protocol: rule.protocol || 'tcp', action: rule.action || 'allow', port_range: rule.port_range || '', ip_range: rule.ip_range || '', priority: rule.priority || 0 })
|
||||
Object.assign(ruleForm, { id: rule.id, group_id: sgId.value, port_group_id: sgId.value, protocol: rule.protocol || 'tcp', action: rule.action || 'allow', port_range: rule.port_range || '', ip_range: rule.ip_range || '', priority: rule.priority || 0, description: rule.description || '' })
|
||||
ruleDialogVisible.value = true
|
||||
}
|
||||
const submitRule = () => {
|
||||
|
||||
@@ -433,7 +433,7 @@ const handleApply = (row) => {
|
||||
}
|
||||
|
||||
const handleGoDetail = (row) => {
|
||||
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, id: row.id } })
|
||||
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, sg_id: row.id } })
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<el-dropdown-item divided command="rebuild">重装虚拟机</el-dropdown-item>
|
||||
<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-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -129,7 +130,7 @@
|
||||
<div class="config-row">
|
||||
<div class="config-cell">
|
||||
<span class="config-label">流量上限(GB)</span>
|
||||
<span class="config-value">{{ detail.traffic_max != null ? (detail.traffic_max / 1024 / 1024).toFixed(2) : '-' }}</span>
|
||||
<span class="config-value">{{ detail.traffic_max != null ? (detail.traffic_max / 1024).toFixed(2) : '-' }}</span>
|
||||
</div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">快照配额</span>
|
||||
@@ -399,6 +400,63 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="组网管理" name="userNetworking">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">用户组网</h3>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<el-button size="small" type="primary" @click="handleVnCreate"><el-icon><Plus /></el-icon>创建组网</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="loadVmNetworkingList" :loading="vnLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="vnList" v-loading="vnLoading" stripe size="small">
|
||||
<el-table-column label="组网" min-width="130">
|
||||
<template #default="{ row }">{{ row.networking?.name || '-' }} <span style="color:#909399">(ID: {{ row.networking?.id }})</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.network" type="success" size="small">已加入</el-tag>
|
||||
<el-tag v-else type="info" size="small">未加入</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="网桥" width="130">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.network?.bridge_name || row.networking?.bridge_name || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP地址" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.network" class="mono-text">{{ row.network.address || '-' }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="网关" width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.network" class="mono-text">{{ row.network.gateway || '-' }}</span>
|
||||
<span v-else class="mono-text" style="color:#c0c4cc">{{ row.networking?.gateway || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="MAC地址" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.network" class="mono-text">{{ row.network.mac_address || '-' }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.network" :type="row.network.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.network.type || '-' }}</el-tag>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.network" link type="primary" size="small" @click="handleJoinNetworking(row)">加入</el-button>
|
||||
<el-button v-else link type="danger" size="small" @click="handleLeaveNetworking(row)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!vnList.length && !vnLoading" description="没有匹配的可用组网" :image-size="60" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="监控" name="monitor">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
@@ -532,19 +590,20 @@
|
||||
<el-input-number v-model="editForm.ssh_port" :min="1" :max="65535" controls-position="right" style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网络">
|
||||
<div style="width: 100%">
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="editSelectedNetworks.length">
|
||||
<el-tag v-for="n in editSelectedNetworks" :key="n.id" closable size="small" @close="removeEditNetwork(n.id)">
|
||||
{{ n.name }} (ID:{{ n.id }})
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button size="small" @click="showEditNetworkSelector = true">选择网络</el-button>
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
|
||||
<el-tag v-if="editSelectedNetworks.length" closable @close="removeEditNetwork(editSelectedNetworks[0].id)">
|
||||
{{ editSelectedNetworks[0].name }} (ID:{{ editSelectedNetworks[0].id }})
|
||||
</el-tag>
|
||||
<el-button size="small" @click="showEditNetworkSelector = true">{{ editSelectedNetworks.length ? '更换网络' : '选择网络' }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="内网组网">
|
||||
<el-select v-model="editForm.internet_network_id" placeholder="选择内网组网(可选)" clearable filterable style="width: 100%">
|
||||
<el-option v-for="n in networkingOptions" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
|
||||
</el-select>
|
||||
<el-form-item label="内网">
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
|
||||
<el-tag v-if="editSelectedInternalNetworks.length" closable @close="removeEditInternalNetwork(editSelectedInternalNetworks[0].id)">
|
||||
{{ editSelectedInternalNetworks[0].name }} (ID:{{ editSelectedInternalNetworks[0].id }})
|
||||
</el-tag>
|
||||
<el-button size="small" @click="showEditInternalNetworkSelector = true">{{ editSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="安全组">
|
||||
<el-select v-model="editForm.port_group_id" placeholder="选择安全组(可选)" filterable clearable style="width: 100%">
|
||||
@@ -557,8 +616,10 @@
|
||||
<el-button type="primary" :loading="actionLoading" @click="submitEditVm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 编辑用网络选择器 -->
|
||||
<NetworkSelectorPopup v-model="showEditNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleEditNetworkConfirm" />
|
||||
<!-- 编辑用网络选择器(外网 bridge) -->
|
||||
<NetworkSelectorPopup v-model="showEditNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleEditNetworkConfirm" @create="() => handleNetCreate('edit')" />
|
||||
<!-- 编辑用内网选择器(内网 nat) -->
|
||||
<NetworkSelectorPopup v-model="showEditInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleEditInternalNetworkConfirm" @create="() => handleNetCreate('editInternal')" />
|
||||
|
||||
<!-- 重构虚拟机弹窗 -->
|
||||
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="650px" destroy-on-close>
|
||||
@@ -627,19 +688,20 @@
|
||||
<el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="网络">
|
||||
<div style="width: 100%">
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="refactorSelectedNetworks.length">
|
||||
<el-tag v-for="n in refactorSelectedNetworks" :key="n.id" closable size="small" @close="removeRefactorNetwork(n.id)">
|
||||
{{ n.name }} (ID:{{ n.id }})
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button size="small" @click="showRefactorNetworkSelector = true">选择网络</el-button>
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
|
||||
<el-tag v-if="refactorSelectedNetworks.length" closable @close="removeRefactorNetwork(refactorSelectedNetworks[0].id)">
|
||||
{{ refactorSelectedNetworks[0].name }} (ID:{{ refactorSelectedNetworks[0].id }})
|
||||
</el-tag>
|
||||
<el-button size="small" @click="showRefactorNetworkSelector = true">{{ refactorSelectedNetworks.length ? '更换网络' : '选择网络' }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="内网组网">
|
||||
<el-select v-model="refactorForm.internet_network_id" placeholder="选择内网组网(可选)" clearable filterable style="width: 100%">
|
||||
<el-option v-for="n in networkingOptions" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
|
||||
</el-select>
|
||||
<el-form-item label="内网">
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
|
||||
<el-tag v-if="refactorSelectedInternalNetworks.length" closable @close="removeRefactorInternalNetwork(refactorSelectedInternalNetworks[0].id)">
|
||||
{{ refactorSelectedInternalNetworks[0].name }} (ID:{{ refactorSelectedInternalNetworks[0].id }})
|
||||
</el-tag>
|
||||
<el-button size="small" @click="showRefactorInternalNetworkSelector = true">{{ refactorSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="安全组">
|
||||
<el-select v-model="refactorForm.port_group_id" placeholder="选择安全组(可选)" filterable clearable style="width: 100%">
|
||||
@@ -653,8 +715,10 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重构用网络选择器 -->
|
||||
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleRefactorNetworkConfirm" />
|
||||
<!-- 重构用网络选择器(外网 bridge) -->
|
||||
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleRefactorNetworkConfirm" @create="() => handleNetCreate('refactor')" />
|
||||
<!-- 重构用内网选择器(内网 nat) -->
|
||||
<NetworkSelectorPopup v-model="showRefactorInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleRefactorInternalNetworkConfirm" @create="() => handleNetCreate('refactorInternal')" />
|
||||
|
||||
<!-- VNC 连接弹窗 -->
|
||||
<el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close>
|
||||
@@ -690,7 +754,7 @@
|
||||
<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="每月最大流量(KB)">
|
||||
<el-form-item label="每月最大流量(MB)">
|
||||
<el-input-number v-model="trafficForm.traffic_max" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -700,8 +764,50 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 迁移虚拟机弹窗 -->
|
||||
<el-dialog v-model="migrateDialogVisible" title="迁移虚拟机(更换宿主机)" width="560px" destroy-on-close>
|
||||
<el-alert type="warning" :closable="false" style="margin-bottom: 16px">将虚拟机迁移到其他宿主机或宿主机组,请谨慎操作!</el-alert>
|
||||
<el-form label-width="130px" v-loading="migrateOptionsLoading">
|
||||
<el-form-item label="当前宿主机">
|
||||
<el-tag>{{ detail?.host_name || detail?.host_id || '-' }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="迁移目标方式">
|
||||
<el-radio-group v-model="migrateMode">
|
||||
<el-radio value="host">指定宿主机</el-radio>
|
||||
<el-radio value="group">指定宿主机组</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标宿主机" v-if="migrateMode === 'host'">
|
||||
<el-select v-model="migrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width: 100%">
|
||||
<el-option v-for="h in migrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" :disabled="h.id === detail?.host_id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标宿主机组" v-if="migrateMode === 'group'">
|
||||
<el-select v-model="migrateForm.target_host_group_id" placeholder="选择目标宿主机组" filterable style="width: 100%">
|
||||
<el-option v-for="g in migrateGroupOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="新IPv4数量">
|
||||
<el-input-number v-model="migrateForm.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="migrateForm.ipv6_num" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="migrateDialogVisible = false">取消</el-button>
|
||||
<el-button type="warning" :loading="actionLoading" @click="submitMigrateVm">确定迁移</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 绑定网络选择器 -->
|
||||
<NetworkSelectorPopup v-model="showNetBindSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleNetBindConfirm" @create="handleNetCreate" />
|
||||
<NetworkSelectorPopup v-model="showNetBindSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindConfirm" @create="() => handleNetCreate('bind')" />
|
||||
|
||||
<!-- 创建/编辑网络弹窗 -->
|
||||
<el-dialog v-model="netDialogVisible" :title="netDialogType === 'add' ? '创建网络' : '编辑网络'" width="600px" destroy-on-close>
|
||||
@@ -723,7 +829,7 @@
|
||||
<el-form-item label="逻辑端口名"><el-input v-model="netForm.ls_name" placeholder="不填使用默认" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="netDialogVisible = false">取消</el-button>
|
||||
<el-button @click="handleNetDialogCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="actionLoading" @click="submitNetForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -968,6 +1074,47 @@
|
||||
<el-button type="primary" :loading="sgSubmitLoading" @click="submitSgRule">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 加入组网弹窗 -->
|
||||
<el-dialog v-model="vnJoinVisible" title="加入组网" width="480px" destroy-on-close>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="组网">{{ vnJoinTarget?.name || '-' }} (ID: {{ vnJoinTarget?.id }})</el-form-item>
|
||||
<el-form-item label="网桥"><span class="mono-text">{{ vnJoinTarget?.bridge_name || '-' }}</span></el-form-item>
|
||||
<el-form-item label="网关"><span class="mono-text">{{ vnJoinTarget?.gateway || '-' }}</span></el-form-item>
|
||||
<el-form-item label="指定IP">
|
||||
<el-input v-model="vnJoinIp" placeholder="留空自动分配" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="vnJoinVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="vnSubmitLoading" @click="submitJoinNetworking">加入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 创建组网弹窗 -->
|
||||
<el-dialog v-model="vnCreateVisible" title="创建组网" width="500px" destroy-on-close>
|
||||
<el-form ref="vnCreateFormRef" :model="vnCreateForm" :rules="vnCreateRules" label-width="100px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="vnCreateForm.name" placeholder="组网名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网桥名称" prop="bridge_name">
|
||||
<el-input v-model="vnCreateForm.bridge_name" placeholder="网桥名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关">
|
||||
<el-input v-model="vnCreateForm.gateway" placeholder="可选,如 10.0.0.1/24" />
|
||||
</el-form-item>
|
||||
<el-form-item label="宿主机">
|
||||
<span style="color: #606266">{{ vnCreateHostName || '加载中...' }} <span style="color: #909399">(ID: {{ vmHostId }})</span></span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户">
|
||||
<span style="color: #606266">{{ vnCreateUserName || '加载中...' }} <span style="color: #909399">(ID: {{ detail?.user_id || '-' }})</span></span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="vnCreateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="vnSubmitLoading" @click="submitVnCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -981,7 +1128,7 @@ import {
|
||||
startVm, stopVm, rebootVm, suspendVm, resumeVm,
|
||||
rebuildVm, refactorVm, updateVm, updateVmTraffic, rescueVm, exitRescueVm,
|
||||
getVmVnc, getVncNodeList,
|
||||
getSecurityGroupList, getUserNetworkingList,
|
||||
getSecurityGroupList, getUserNetworkingList, createUserNetworking, assignUserNetworking, removeUserNetworkingNetwork,
|
||||
getRemoteHostList, getSecurityGroupDetail, createSecurityGroup,
|
||||
syncSecurityGroup, deleteSecurityGroup,
|
||||
enableSecurityGroupWhitelist, disableSecurityGroupWhitelist,
|
||||
@@ -990,8 +1137,10 @@ import {
|
||||
createVolume, resizeVolume, mountVolume, unmountVolume, transferVolume, deleteVolume,
|
||||
getVmList, bindSecurityGroup, unbindSecurityGroup,
|
||||
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
|
||||
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit
|
||||
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
|
||||
migrateVm, getRemoteHostGroupList, getRemoteHostDetail
|
||||
} from '@/api/admin/kvmService'
|
||||
import { getUserInfo } from '@/api/admin/user'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import * as echarts from 'echarts'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
@@ -1007,7 +1156,7 @@ const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const serviceId = computed(() => parseInt(route.query.service_id) || 0)
|
||||
const serviceName = computed(() => route.query.service_name || '')
|
||||
const vmId = computed(() => parseInt(route.query.vm_id) || parseInt(route.query.id) || 0)
|
||||
const vmId = computed(() => parseInt(route.query.vm_id) || 0)
|
||||
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
@@ -1074,7 +1223,8 @@ const handleMoreCommand = (cmd) => {
|
||||
if (powerActions.includes(cmd)) { handlePower(cmd); return }
|
||||
const actionMap = {
|
||||
editVm: handleEditVm, refactorVm: handleRefactorVm, updateTraffic: handleUpdateTraffic,
|
||||
rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue
|
||||
rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue,
|
||||
migrateVm: handleMigrateVm
|
||||
}
|
||||
if (actionMap[cmd]) actionMap[cmd]()
|
||||
}
|
||||
@@ -1337,30 +1487,24 @@ const editFormRef = ref(null)
|
||||
const editForm = reactive({
|
||||
rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
root_password: '', ssh_port: 22,
|
||||
internet_network_id: 0, port_group_id: 0
|
||||
port_group_id: ''
|
||||
})
|
||||
const editSelectedNetworks = ref([])
|
||||
const showEditNetworkSelector = ref(false)
|
||||
const editSelectedInternalNetworks = ref([])
|
||||
const showEditInternalNetworkSelector = ref(false)
|
||||
|
||||
const handleEditNetworkConfirm = (network) => {
|
||||
if (!editSelectedNetworks.value.find(n => n.id === network.id)) {
|
||||
editSelectedNetworks.value.push({ id: network.id, name: network.name })
|
||||
}
|
||||
editSelectedNetworks.value = [{ id: network.id, name: network.name }]
|
||||
}
|
||||
const removeEditNetwork = (id) => {
|
||||
editSelectedNetworks.value = editSelectedNetworks.value.filter(n => n.id !== id)
|
||||
if (editForm.internet_network_id === id) editForm.internet_network_id = 0
|
||||
}
|
||||
|
||||
const networkingOptions = ref([])
|
||||
const loadNetworkingOptions = async () => {
|
||||
try {
|
||||
const res = await getUserNetworkingList({ service_id: serviceId.value, page: 1, count: 10 })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
networkingOptions.value = Array.isArray(inner) ? inner : (inner.data || [])
|
||||
}
|
||||
} catch { /* */ }
|
||||
const handleEditInternalNetworkConfirm = (network) => {
|
||||
editSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
|
||||
}
|
||||
const removeEditInternalNetwork = (id) => {
|
||||
editSelectedInternalNetworks.value = editSelectedInternalNetworks.value.filter(n => n.id !== id)
|
||||
}
|
||||
|
||||
const handleEditVm = async () => {
|
||||
@@ -1371,16 +1515,17 @@ const handleEditVm = async () => {
|
||||
tx_bandwidth: d.tx_bandwidth || 0,
|
||||
root_password: '',
|
||||
ssh_port: d.ssh_port || 22,
|
||||
internet_network_id: '',
|
||||
port_group_id: vmPortGroup.value?.id || 0
|
||||
port_group_id: vmPortGroup.value?.id || ''
|
||||
})
|
||||
editSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name }))
|
||||
const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
|
||||
const natNets = vmNetworks.value.filter(n => n.type === 'nat')
|
||||
editSelectedNetworks.value = bridgeNets.length ? [{ id: bridgeNets[0].id, name: bridgeNets[0].name }] : []
|
||||
editSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
|
||||
editDialogVisible.value = true
|
||||
dialogOptionsLoading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
!sgOptions.value.length ? loadSgOptions() : Promise.resolve(),
|
||||
loadNetworkingOptions()
|
||||
!sgOptions.value.length ? loadSgOptions() : Promise.resolve()
|
||||
])
|
||||
} finally { dialogOptionsLoading.value = false }
|
||||
}
|
||||
@@ -1396,7 +1541,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))
|
||||
if (editForm.internet_network_id) fd.append('internet_network_id', editForm.internet_network_id)
|
||||
editSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.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() }
|
||||
@@ -1411,12 +1556,14 @@ const refactorForm = reactive({
|
||||
memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '',
|
||||
ssh_port: 0, vnc_port: 0, vnc_password: '',
|
||||
internet_network_id: 0, port_group_id: 0
|
||||
port_group_id: ''
|
||||
})
|
||||
const refactorMemUnit = ref(1048576)
|
||||
const refactorMemDisplay = ref(0)
|
||||
const refactorSelectedNetworks = ref([])
|
||||
const showRefactorNetworkSelector = ref(false)
|
||||
const refactorSelectedInternalNetworks = ref([])
|
||||
const showRefactorInternalNetworkSelector = ref(false)
|
||||
|
||||
const onRefactorMemUnitChange = () => {
|
||||
refactorMemDisplay.value = refactorForm.memory ? Math.round(refactorForm.memory / refactorMemUnit.value * 100) / 100 : 0
|
||||
@@ -1424,13 +1571,16 @@ const onRefactorMemUnitChange = () => {
|
||||
watch(refactorMemDisplay, (v) => { refactorForm.memory = Math.round(v * refactorMemUnit.value) })
|
||||
|
||||
const handleRefactorNetworkConfirm = (network) => {
|
||||
if (!refactorSelectedNetworks.value.find(n => n.id === network.id)) {
|
||||
refactorSelectedNetworks.value.push({ id: network.id, name: network.name })
|
||||
}
|
||||
refactorSelectedNetworks.value = [{ id: network.id, name: network.name }]
|
||||
}
|
||||
const removeRefactorNetwork = (id) => {
|
||||
refactorSelectedNetworks.value = refactorSelectedNetworks.value.filter(n => n.id !== id)
|
||||
if (refactorForm.internet_network_id === id) refactorForm.internet_network_id = 0
|
||||
}
|
||||
const handleRefactorInternalNetworkConfirm = (network) => {
|
||||
refactorSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
|
||||
}
|
||||
const removeRefactorInternalNetwork = (id) => {
|
||||
refactorSelectedInternalNetworks.value = refactorSelectedInternalNetworks.value.filter(n => n.id !== id)
|
||||
}
|
||||
|
||||
const handleRefactorVm = async () => {
|
||||
@@ -1443,10 +1593,12 @@ const handleRefactorVm = async () => {
|
||||
uuid: d.uuid || '', mate_data_id: d.mate_data_id || '',
|
||||
physical_name: d.physical_name || '', config_path: d.config_path || '',
|
||||
ssh_port: d.ssh_port || 0, vnc_port: 0, vnc_password: '',
|
||||
internet_network_id: 0,
|
||||
port_group_id: vmPortGroup.value?.id || 0
|
||||
port_group_id: vmPortGroup.value?.id || ''
|
||||
})
|
||||
refactorSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name }))
|
||||
const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
|
||||
const natNets = vmNetworks.value.filter(n => n.type === 'nat')
|
||||
refactorSelectedNetworks.value = bridgeNets.length ? [{ id: bridgeNets[0].id, name: bridgeNets[0].name }] : []
|
||||
refactorSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
|
||||
const mem = d.memory || 0
|
||||
if (mem >= 1048576 && mem % 1048576 === 0) { refactorMemUnit.value = 1048576; refactorMemDisplay.value = mem / 1048576 }
|
||||
else if (mem >= 1024 && mem % 1024 === 0) { refactorMemUnit.value = 1024; refactorMemDisplay.value = mem / 1024 }
|
||||
@@ -1455,8 +1607,7 @@ const handleRefactorVm = async () => {
|
||||
dialogOptionsLoading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
!sgOptions.value.length ? loadSgOptions() : Promise.resolve(),
|
||||
loadNetworkingOptions()
|
||||
!sgOptions.value.length ? loadSgOptions() : Promise.resolve()
|
||||
])
|
||||
} finally { dialogOptionsLoading.value = false }
|
||||
}
|
||||
@@ -1480,7 +1631,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))
|
||||
if (refactorForm.internet_network_id) fd.append('internet_network_id', refactorForm.internet_network_id)
|
||||
refactorSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.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() }
|
||||
@@ -1517,6 +1668,53 @@ const submitUpdateTraffic = async () => {
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- 迁移虚拟机 ----
|
||||
const migrateDialogVisible = ref(false)
|
||||
const migrateOptionsLoading = ref(false)
|
||||
const migrateMode = ref('host')
|
||||
const migrateHostOptions = ref([])
|
||||
const migrateGroupOptions = ref([])
|
||||
const migrateForm = reactive({ target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||
|
||||
const handleMigrateVm = async () => {
|
||||
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||
migrateMode.value = 'host'
|
||||
migrateDialogVisible.value = true
|
||||
migrateOptionsLoading.value = true
|
||||
try {
|
||||
const [hostRes, groupRes] = await Promise.all([
|
||||
getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 }),
|
||||
getRemoteHostGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||
])
|
||||
if (hostRes?.data?.code === 200 && hostRes?.data?.data) {
|
||||
const inner = hostRes.data.data
|
||||
migrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
|
||||
}
|
||||
if (groupRes?.data?.code === 200 && groupRes?.data?.data) {
|
||||
const inner = groupRes.data.data
|
||||
migrateGroupOptions.value = Array.isArray(inner) ? inner : (inner.host_groups || inner.data || [])
|
||||
}
|
||||
} catch { /* */ } finally { migrateOptionsLoading.value = false }
|
||||
}
|
||||
|
||||
const submitMigrateVm = async () => {
|
||||
if (migrateMode.value === 'host' && !migrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
|
||||
if (migrateMode.value === 'group' && !migrateForm.target_host_group_id) { ElMessage.warning('请选择目标宿主机组'); return }
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('vm_id', vmId.value)
|
||||
if (migrateMode.value === 'host') fd.append('target_host_id', migrateForm.target_host_id)
|
||||
else fd.append('target_host_group_id', migrateForm.target_host_group_id)
|
||||
if (migrateForm.ipv4_num) fd.append('ipv4_num', migrateForm.ipv4_num)
|
||||
if (migrateForm.ipv6_num) fd.append('ipv6_num', migrateForm.ipv6_num)
|
||||
const res = await migrateVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('迁移成功'); migrateDialogVisible.value = false; loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '迁移失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- VNC 连接 ----
|
||||
const vncDialogVisible = ref(false)
|
||||
const vncNodeId = ref(null)
|
||||
@@ -1560,7 +1758,7 @@ const sgOptions = ref([])
|
||||
|
||||
const loadSgOptions = async () => {
|
||||
try {
|
||||
const res = await getSecurityGroupList({ service_id: serviceId.value, page: 1, page_size: 200 })
|
||||
const res = await getSecurityGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
sgOptions.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
@@ -1604,6 +1802,7 @@ const handleNetBindConfirm = async (selectedNetwork) => {
|
||||
// ---- 网络操作(创建/编辑/删除/详情) ----
|
||||
const netDialogVisible = ref(false)
|
||||
const netDialogType = ref('add')
|
||||
const netDialogSource = ref('')
|
||||
const netFormRef = ref(null)
|
||||
const netDetailVisible = ref(false)
|
||||
const netDetailData = ref(null)
|
||||
@@ -1615,11 +1814,23 @@ const netFormRules = {
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleNetCreate = () => {
|
||||
const handleNetCreate = (source = '') => {
|
||||
netDialogSource.value = source
|
||||
netDialogType.value = 'add'
|
||||
Object.assign(netForm, { id: 0, name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: vmHostId.value })
|
||||
netDialogVisible.value = true
|
||||
}
|
||||
const handleNetDialogCancel = () => {
|
||||
netDialogVisible.value = false
|
||||
const src = netDialogSource.value
|
||||
netDialogSource.value = ''
|
||||
if (src === 'edit') showEditNetworkSelector.value = true
|
||||
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
|
||||
}
|
||||
|
||||
const handleNetEdit = (row) => {
|
||||
netDialogType.value = 'edit'
|
||||
Object.assign(netForm, { id: row.id, name: row.name || '', address: row.address || '', gateway: row.gateway || '', nameservers: row.nameservers || '', type: row.type || 'bridge', mac_address: row.mac_address || '', bridge_name: row.bridge_name || '', ls_bridge_name: row.ls_bridge_name || '', ls_name: row.ls_name || '', host_id: row.host_id || vmHostId.value })
|
||||
@@ -1644,7 +1855,7 @@ const submitNetForm = () => {
|
||||
if (netForm.ls_name) fd.append('ls_name', netForm.ls_name)
|
||||
let res
|
||||
if (netDialogType.value === 'add') { res = await createNetwork(fd) }
|
||||
else { fd.append('network_id', netForm.id); res = await updateNetwork(fd) }
|
||||
else { fd.append('id', netForm.id); res = await updateNetwork(fd) }
|
||||
if (res?.data?.code === 200) { ElMessage.success(netDialogType.value === 'add' ? '创建成功' : '修改成功'); netDialogVisible.value = false; loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '操作失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { actionLoading.value = false }
|
||||
@@ -1867,7 +2078,14 @@ const submitSgCreate = () => {
|
||||
if (!valid) return
|
||||
sgSubmitLoading.value = true
|
||||
try {
|
||||
const res = await createSecurityGroup({ service_id: serviceId.value, ...sgCreateForm })
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('name', sgCreateForm.name)
|
||||
fd.append('host_id', sgCreateForm.host_id)
|
||||
fd.append('direction', sgCreateForm.direction)
|
||||
if (sgCreateForm.lock) fd.append('lock', true)
|
||||
if (sgCreateForm.drop_all) fd.append('drop_all', true)
|
||||
const res = await createSecurityGroup(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
sgCreateDialogVisible.value = false
|
||||
@@ -1894,7 +2112,8 @@ const submitSgSync = async () => {
|
||||
if (!sgSyncHostId.value) { ElMessage.warning('请选择宿主机'); return }
|
||||
sgSubmitLoading.value = true
|
||||
try {
|
||||
const res = await syncSecurityGroup({ service_id: serviceId.value, id: sgSyncTarget.value.id, host_id: sgSyncHostId.value })
|
||||
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgSyncTarget.value.id); fd.append('host_id', sgSyncHostId.value)
|
||||
const res = await syncSecurityGroup(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('同步成功')
|
||||
sgSyncDialogVisible.value = false
|
||||
@@ -1909,7 +2128,8 @@ const handleSgApply = (row) => {
|
||||
confirmButtonText: '确定应用', cancelButtonText: '取消', type: 'info'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await applySecurityGroup({ service_id: serviceId.value, id: row.id })
|
||||
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
|
||||
const res = await applySecurityGroup(fd)
|
||||
if (res?.data?.code === 200) ElMessage.success('应用成功')
|
||||
else ElMessage.error(extractApiError(res?.data, '应用失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '应用失败')) }
|
||||
@@ -1918,7 +2138,7 @@ const handleSgApply = (row) => {
|
||||
|
||||
// 编辑(跳转安全组详情页)
|
||||
const handleSgGoDetail = (row) => {
|
||||
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, id: row.id } })
|
||||
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, sg_id: row.id } })
|
||||
}
|
||||
|
||||
// 白名单切换
|
||||
@@ -1929,7 +2149,8 @@ const handleSgToggleWhitelist = (row) => {
|
||||
}).then(async () => {
|
||||
try {
|
||||
const api = row.drop_all ? disableSecurityGroupWhitelist : enableSecurityGroupWhitelist
|
||||
const res = await api({ service_id: serviceId.value, id: row.id })
|
||||
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
|
||||
const res = await api(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success(`${action}成功`); loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, `${action}失败`))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${action}失败`)) }
|
||||
@@ -1965,7 +2186,8 @@ const submitSgVmBind = async () => {
|
||||
sgSubmitLoading.value = true
|
||||
try {
|
||||
const api = sgVmBindType.value === 'bind' ? bindSecurityGroup : unbindSecurityGroup
|
||||
const res = await api({ service_id: serviceId.value, id: sgVmBindTarget.value.id, vm_id: vmId.value })
|
||||
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgVmBindTarget.value.id); fd.append('vm_id', vmId.value)
|
||||
const res = await api(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success(sgVmBindType.value === 'bind' ? '绑定成功' : '解绑成功')
|
||||
sgVmBindDialogVisible.value = false
|
||||
@@ -1994,20 +2216,20 @@ const handleSgViewDetail = async (row) => {
|
||||
const sgRuleDialogVisible = ref(false)
|
||||
const sgRuleDialogType = ref('add')
|
||||
const sgRuleFormRef = ref(null)
|
||||
const sgRuleForm = reactive({ id: undefined, group_id: 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0 })
|
||||
const sgRuleForm = reactive({ id: undefined, group_id: 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
|
||||
const sgRuleRules = {
|
||||
protocol: [{ required: true, message: '请选择协议', trigger: 'change' }],
|
||||
action: [{ required: true, message: '请选择动作', trigger: 'change' }]
|
||||
}
|
||||
const handleSgAddRule = () => {
|
||||
sgRuleDialogType.value = 'add'
|
||||
Object.assign(sgRuleForm, { id: undefined, group_id: sgCurrentDetail.value?.id || 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0 })
|
||||
Object.assign(sgRuleForm, { id: undefined, group_id: sgCurrentDetail.value?.id || 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
|
||||
sgRuleDialogVisible.value = true
|
||||
}
|
||||
const handleSgEditRule = (rule) => {
|
||||
sgRuleDialogType.value = 'edit'
|
||||
Object.assign(sgRuleForm, {
|
||||
id: rule.id, group_id: sgCurrentDetail.value?.id || 0, port_group_id: sgCurrentDetail.value?.id || 0,
|
||||
id: rule.id, group_id: sgCurrentDetail.value?.id || 0, port_group_id: sgCurrentDetail.value?.id || '',
|
||||
protocol: rule.protocol || 'tcp', action: rule.action || 'allow',
|
||||
port_range: rule.port_range || '', ip_range: rule.ip_range || '', priority: rule.priority || 0
|
||||
})
|
||||
@@ -2018,9 +2240,19 @@ const submitSgRule = () => {
|
||||
if (!valid) return
|
||||
sgSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('group_id', sgRuleForm.group_id)
|
||||
if (sgRuleForm.id) fd.append('id', sgRuleForm.id)
|
||||
if (sgRuleForm.port_group_id) fd.append('port_group_id', sgRuleForm.port_group_id)
|
||||
fd.append('protocol', sgRuleForm.protocol)
|
||||
fd.append('action', sgRuleForm.action)
|
||||
if (sgRuleForm.port_range) fd.append('port_range', sgRuleForm.port_range)
|
||||
if (sgRuleForm.ip_range) fd.append('ip_range', sgRuleForm.ip_range)
|
||||
fd.append('priority', sgRuleForm.priority || 0)
|
||||
const res = sgRuleDialogType.value === 'add'
|
||||
? await createSecurityGroupRule({ service_id: serviceId.value, ...sgRuleForm })
|
||||
: await updateSecurityGroupRule({ service_id: serviceId.value, ...sgRuleForm })
|
||||
? await createSecurityGroupRule(fd)
|
||||
: await updateSecurityGroupRule(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success(sgRuleDialogType.value === 'add' ? '规则创建成功' : '规则修改成功')
|
||||
sgRuleDialogVisible.value = false
|
||||
@@ -2297,6 +2529,162 @@ const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// ---- 组网管理 ----
|
||||
const vnLoading = ref(false)
|
||||
const vnSubmitLoading = ref(false)
|
||||
const vnList = ref([])
|
||||
const vnJoinVisible = ref(false)
|
||||
const vnJoinTarget = ref(null)
|
||||
const vnJoinIp = ref('')
|
||||
|
||||
const vmBridgeNameMap = computed(() => {
|
||||
const map = {}
|
||||
for (const n of vmNetworks.value) {
|
||||
if (n.bridge_name) map[n.bridge_name] = n
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const loadVmNetworkingList = async () => {
|
||||
if (!detail.value) return
|
||||
vnLoading.value = true
|
||||
vnList.value = []
|
||||
try {
|
||||
const userId = detail.value.user_id
|
||||
const hostId = vmHostId.value
|
||||
if (!userId || !hostId) { vnLoading.value = false; return }
|
||||
const res = await getUserNetworkingList({
|
||||
service_id: serviceId.value, page: 1, count: 10,
|
||||
host_id: hostId, user_id: userId
|
||||
})
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const inner = res.data.data
|
||||
const networkings = Array.isArray(inner) ? inner : (inner.data || [])
|
||||
const results = []
|
||||
for (const nw of networkings) {
|
||||
const matchedVmNet = nw.bridge_name ? vmBridgeNameMap.value[nw.bridge_name] : null
|
||||
results.push({ networking: nw, network: matchedVmNet || null })
|
||||
}
|
||||
vnList.value = results
|
||||
}
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网信息失败')) }
|
||||
finally { vnLoading.value = false }
|
||||
}
|
||||
|
||||
// 创建组网
|
||||
const vnCreateVisible = ref(false)
|
||||
const vnCreateFormRef = ref(null)
|
||||
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' }]
|
||||
}
|
||||
|
||||
const handleVnCreate = async () => {
|
||||
Object.assign(vnCreateForm, { name: '', bridge_name: '', gateway: '' })
|
||||
vnCreateHostName.value = ''
|
||||
vnCreateUserName.value = ''
|
||||
vnCreateVisible.value = true
|
||||
const userId = detail.value?.user_id
|
||||
const hostId = vmHostId.value
|
||||
const requests = []
|
||||
if (userId) {
|
||||
requests.push(
|
||||
getUserInfo({ user_id: userId }).then(res => {
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
vnCreateUserName.value = res.data.data.UserName || ''
|
||||
}
|
||||
}).catch(() => {})
|
||||
)
|
||||
}
|
||||
if (hostId) {
|
||||
requests.push(
|
||||
getRemoteHostDetail({ service_id: serviceId.value, id: hostId }).then(res => {
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const h = res.data.data.host ?? res.data.data
|
||||
vnCreateHostName.value = h.name || ''
|
||||
}
|
||||
}).catch(() => {})
|
||||
)
|
||||
}
|
||||
await Promise.all(requests)
|
||||
}
|
||||
|
||||
const submitVnCreate = () => {
|
||||
vnCreateFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
vnSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('name', vnCreateForm.name)
|
||||
fd.append('bridge_name', vnCreateForm.bridge_name)
|
||||
fd.append('host_id', vmHostId.value)
|
||||
fd.append('user_id', detail.value?.user_id || 0)
|
||||
if (vnCreateForm.gateway) fd.append('gateway', vnCreateForm.gateway)
|
||||
const res = await createUserNetworking(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建组网成功')
|
||||
vnCreateVisible.value = false
|
||||
loadVmNetworkingList()
|
||||
} else ElMessage.error(extractApiError(res?.data, '创建组网失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建组网失败')) }
|
||||
finally { vnSubmitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
const handleJoinNetworking = (row) => {
|
||||
vnJoinTarget.value = row.networking
|
||||
vnJoinIp.value = ''
|
||||
vnJoinVisible.value = true
|
||||
}
|
||||
|
||||
const submitJoinNetworking = async () => {
|
||||
if (!vnJoinTarget.value) return
|
||||
vnSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('networking_id', vnJoinTarget.value.id)
|
||||
fd.append('vm_id', vmId.value)
|
||||
if (vnJoinIp.value.trim()) fd.append('ip', vnJoinIp.value.trim())
|
||||
const res = await assignUserNetworking(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('加入组网成功')
|
||||
vnJoinVisible.value = false
|
||||
loadVmNetworkingList()
|
||||
loadDetail()
|
||||
} else ElMessage.error(extractApiError(res?.data, '加入组网失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加入组网失败')) }
|
||||
finally { vnSubmitLoading.value = false }
|
||||
}
|
||||
|
||||
const handleLeaveNetworking = (row) => {
|
||||
const netId = row.network?.id
|
||||
const networkingId = row.networking?.id
|
||||
if (!netId || !networkingId) { ElMessage.warning('缺少网络信息'); return }
|
||||
ElMessageBox.confirm(
|
||||
`确定要将该虚拟机从组网「${row.networking?.name || networkingId}」中移除?`,
|
||||
'移除确认', { type: 'warning' }
|
||||
).then(async () => {
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('networking_id', networkingId)
|
||||
fd.append('network_id', netId)
|
||||
fd.append('vm_id', vmId.value)
|
||||
const res = await removeUserNetworkingNetwork(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('已移除')
|
||||
loadVmNetworkingList()
|
||||
loadDetail()
|
||||
} else ElMessage.error(extractApiError(res?.data, '移除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
let loadedVmId = null
|
||||
const initPage = () => {
|
||||
if (!vmId.value || loadedVmId === vmId.value) return
|
||||
@@ -2316,6 +2704,7 @@ watch(activeTab, (tab) => {
|
||||
else stopPolling()
|
||||
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
|
||||
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
||||
if (tab === 'userNetworking') loadVmNetworkingList()
|
||||
})
|
||||
onActivated(() => {
|
||||
isPageActive = true
|
||||
|
||||
@@ -174,9 +174,18 @@
|
||||
<el-radio value="ids">选择网络IP</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP数量" v-if="ipMode === 'num'">
|
||||
<el-input-number v-model="createForm.ip_num" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16" v-if="ipMode === 'num'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="IPv4数量">
|
||||
<el-input-number v-model="createForm.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="createForm.ipv6_num" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="网络IP列表" v-if="ipMode === 'ids'">
|
||||
<el-select v-model="createForm.network_ids" multiple filterable placeholder="选择可用网络IP" style="width: 100%">
|
||||
<el-option v-for="n in networkOptions" :key="n.id" :label="`${n.name || ''} - ${n.address || n.ip || ''}`" :value="n.id" />
|
||||
@@ -447,7 +456,7 @@ const vmMetricsData = ref(null)
|
||||
const createForm = reactive({
|
||||
name: '', host_id: null, image_id: 0, vcpu: 0, memory: 0,
|
||||
system_size: 0, rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
host_group_id: null, user_id: 0, ip_num: 0, network_ids: [],
|
||||
host_group_id: null, user_id: 0, ipv4_num: 0, ipv6_num: 0, network_ids: [],
|
||||
_imageName: '', _groupName: '', _userName: ''
|
||||
})
|
||||
|
||||
@@ -513,6 +522,7 @@ const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: serviceId.value, page: queryParams.page, page_size: queryParams.page_size }
|
||||
if (hostId.value) params.host_id = hostId.value
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
const res = await getVmList(params)
|
||||
@@ -531,7 +541,7 @@ const handleAdd = () => {
|
||||
Object.assign(createForm, {
|
||||
name: '', host_id: injectedHostId?.value || null, image_id: 0,
|
||||
vcpu: 0, memory: 0, system_size: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: null, user_id: 0, ip_num: 0, network_ids: [],
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: null, user_id: 0, ipv4_num: 0, ipv6_num: 0, network_ids: [],
|
||||
_imageName: '', _groupName: '', _userName: ''
|
||||
})
|
||||
memoryUnit.value = 'GB'
|
||||
@@ -549,7 +559,7 @@ const submitCreate = () => {
|
||||
if (hostMode.value === 'host' && !createForm.host_id) { ElMessage.warning('请选择宿主机'); return }
|
||||
if (hostMode.value === 'group' && !createForm.host_group_id) { ElMessage.warning('请选择宿主机组'); return }
|
||||
if (ipMode.value === 'ids' && !createForm.network_ids.length) { ElMessage.warning('请选择网络IP'); return }
|
||||
if (ipMode.value === 'num' && !createForm.ip_num) { ElMessage.warning('请输入IP数量'); return }
|
||||
if (ipMode.value === 'num' && !createForm.ipv4_num && !createForm.ipv6_num) { ElMessage.warning('请输入IPv4或IPv6数量'); return }
|
||||
|
||||
createFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
@@ -567,8 +577,10 @@ const submitCreate = () => {
|
||||
if (createForm.tx_bandwidth) fd.append('tx_bandwidth', createForm.tx_bandwidth)
|
||||
if (hostMode.value === 'host') fd.append('host_id', createForm.host_id)
|
||||
else fd.append('host_group_id', createForm.host_group_id)
|
||||
if (ipMode.value === 'num') fd.append('ip_num', createForm.ip_num)
|
||||
else createForm.network_ids.forEach(id => fd.append('network_ids', id))
|
||||
if (ipMode.value === 'num') {
|
||||
if (createForm.ipv4_num) fd.append('ipv4_num', createForm.ipv4_num)
|
||||
if (createForm.ipv6_num) fd.append('ipv6_num', createForm.ipv6_num)
|
||||
} else createForm.network_ids.forEach(id => fd.append('network_ids', id))
|
||||
const res = await createVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('创建成功'); createDialogVisible.value = false; loadList() }
|
||||
else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||
|
||||
+13
-12
@@ -35,18 +35,19 @@ export default defineConfig(({ mode }) => {
|
||||
host: '0.0.0.0',
|
||||
port: 5176,
|
||||
strictPort: false,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: proxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
},
|
||||
'/acs': {
|
||||
target: proxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
// proxy 已关闭,前端直接请求后端地址(在 src/config/env.js 中配置)
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: proxyTarget,
|
||||
// changeOrigin: true,
|
||||
// secure: false
|
||||
// },
|
||||
// '/acs': {
|
||||
// target: proxyTarget,
|
||||
// changeOrigin: true,
|
||||
// secure: false
|
||||
// }
|
||||
// }
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
|
||||
Reference in New Issue
Block a user