@@ -40,6 +40,9 @@
< el-button type = "primary" size = "small" @click ="openBatchDataMigrate" :loading = "batchLoading" >
< el-icon > < Promotion / > < / el-icon > 批量数据迁移
< / el-button >
< el-button type = "primary" plain size = "small" @click ="openBatchHostMigrate" :loading = "batchLoading" >
< el-icon > < Promotion / > < / el-icon > 批量宿主机迁移
< / el-button >
< el-button type = "danger" size = "small" @click ="handleBatchDelete" :loading = "batchLoading" >
< el-icon > < Delete / > < / el-icon > 批量删除
< / el-button >
@@ -469,6 +472,52 @@
<!-- 用户选择器 -- >
< UserListSelector v-model = "showUserSelector" :current-user-id="createForm.user_id" @confirm="handleUserSelected" / >
<!-- 批量宿主机迁移 -- >
< el-dialog v-model = "batchHostMigrateVisible" title="批量宿主机迁移" width="620px" destroy-on-close class="tk-dialog" >
< el-alert type = "warning" :closable = "false" show -icon style = "margin-bottom:16px" >
将 { { batchHostMigrateTargets . length } } 台虚拟机迁移到当前主控服务下的同一宿主机或宿主机组 。 任务将逐台提交 , 迁移中的实例会被跳过 。
< / el-alert >
< el-form :model = "batchHostMigrateForm" label -width = " 110px " >
< div class = "tk-section" >
< div class = "tk-section-title" > 迁移配置 < / div >
< el-form-item label = "迁移方式" >
< el-radio-group v-model = "batchHostMigrateMode" >
< el-radio value = "host" > 指定宿主机 < / el-radio >
< el-radio value = "group" > 指定宿主机组 < / el-radio >
< / el-radio-group >
< / el-form-item >
< el-form-item v-if = "batchHostMigrateMode === 'host'" label="目标宿主机" required >
< div class = "bind-selector-row" >
< el-input : model -value = " batchHostMigrateForm.target_host_id ? ` $ { batchHostMigrateForm._hostName } ( ID : $ { batchHostMigrateForm.target_host_id } ) ` : ' ' " readonly placeholder = "请选择目标宿主机" / >
< el-button type = "primary" @click ="showBatchHostMigrateHostSelector=true" > 选择 < / el -button >
< / div >
< / el-form-item >
< el-form-item v-else label = "目标宿主机组" required >
< el-select v-model = "batchHostMigrateForm.target_host_group_id" placeholder="请选择目标宿主机组" filterable style="width:100%" :loading="batchHostMigrateOptionsLoading" >
< el-option v-for = "group in batchHostMigrateGroupOptions" :key="group.id" :label="`${group.name} (ID: ${group.id})`" :value="group.id" / >
< / el-select >
< / el-form-item >
< div class = "tk-resource-grid" >
< el-form-item label = "IPv4 数量" > < el-input-number v-model = "batchHostMigrateForm.ipv4_num" :min="0" controls-position="right" /><span class="tk-res-unit" > 个 / 实例 < / span > < / el -form -item >
< el-form-item label = "IPv6 数量" > < el-input-number v-model = "batchHostMigrateForm.ipv6_num" :min="0" controls-position="right" /><span class="tk-res-unit" > 个 / 实例 < / span > < / el -form -item >
< / div >
< el-form-item label = "到期顺延" >
< el-input-number v-model = "batchHostMigrateForm.expire_offset_days" :min="0" :step="1" :precision="0" controls-position="right" / >
< span class = "tk-res-unit" > 天 / 实例 ( 0 不顺延 ) < / span >
< / el-form-item >
< el-form-item v-if = "batchHostMigrateMode === 'host'" label="目标网络" >
< div class = "batch-network-field" >
< div v-if = "batchHostMigrateNetworks.length" class="batch-network-tags"><el-tag v-for="network in batchHostMigrateNetworks" :key="network.id" closable @close="removeBatchHostMigrateNetwork(network.id)" > {{ network.name | | network.id }} ( # {{ network.id }} ) < / el -tag > < / div >
< el-button :disabled = "!batchHostMigrateForm.target_host_id" @click ="showBatchHostMigrateNetworkSelector=true" > 选择网络 < / el -button >
< / div >
< / el-form-item >
< / div >
< / el-form >
< template # footer > < el-button @click ="batchHostMigrateVisible=false" > 取消 < / el -button > < el-button type = "warning" :loading = "batchHostMigrateSubmitting" @click ="submitBatchHostMigrate" > 提交迁移 < / el -button > < / template >
< / el-dialog >
< HostSelectorPopup v-model = "showBatchHostMigrateHostSelector" :service-id="serviceId" :current-id="batchHostMigrateForm.target_host_id || 0" @confirm="handleBatchHostMigrateHostSelected" / >
< NetworkSelectorPopup v-model = "showBatchHostMigrateNetworkSelector" :service-id="serviceId" :host-id="batchHostMigrateForm.target_host_id || 0" filter-type="bridge" filter-used="false" :multiple="true" @confirm="handleBatchHostMigrateNetworkSelected" / >
<!-- 批量数据迁移 -- >
< el-dialog v-model = "batchMigrateVisible" title="批量数据迁移" width="620px" destroy-on-close class="tk-dialog" >
< el-alert type = "warning" :closable = "false" show -icon style = "margin-bottom:16px" >
@@ -496,6 +545,10 @@
< el-form-item label = "IPv4 数量" > < el-input-number v-model = "batchMigrateForm.ipv4_num" :min="0" controls-position="right" /><span class="tk-res-unit" > 个 / 实例 < / span > < / el -form -item >
< el-form-item label = "IPv6 数量" > < el-input-number v-model = "batchMigrateForm.ipv6_num" :min="0" controls-position="right" /><span class="tk-res-unit" > 个 / 实例 < / span > < / el -form -item >
< / div >
< el-form-item label = "到期顺延" >
< el-input-number v-model = "batchMigrateForm.expire_offset_days" :min="0" :step="1" :precision="0" controls-position="right" / >
< span class = "tk-res-unit" > 天 / 实例 ( 0 不顺延 ) < / span >
< / el-form-item >
< el-form-item label = "目标网络" >
< div class = "batch-network-field" >
< div class = "batch-network-tags" v-if = "batchMigrateNetworks.length"><el-tag v-for="network in batchMigrateNetworks" :key="network.id" closable @close="removeBatchMigrateNetwork(network.id)" > {{ network.name | | network.id }} ( # {{ network.id }} ) < / el -tag > < / div >
@@ -541,7 +594,7 @@ import {
getRemoteHostList , getVmList , getVmDetail , getVmStatus ,
createVm , rebuildVm , startVm , stopVm , rebootVm , suspendVm ,
resumeVm , rescueVm , exitRescueVm , deleteVm , getNetworkList , createNetwork , getMetricsHistory ,
getDataMigrateProgress , dataMigrateVm
getDataMigrateProgress , dataMigrateVm , migrateVm , getRemoteHostGroupList
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
@@ -585,6 +638,145 @@ const selectedVms = ref([])
const handleSelectionChange = ( selection ) => { selectedVms . value = selection }
const clearSelection = ( ) => { vmTableRef . value ? . clearSelection ( ) }
/ / 批 量 宿 主 机 迁 移
const batchHostMigrateVisible = ref ( false )
const batchHostMigrateSubmitting = ref ( false )
const batchHostMigrateMode = ref ( 'host' )
const batchHostMigrateTargets = ref ( [ ] )
const batchHostMigrateNetworks = ref ( [ ] )
const batchHostMigrateGroupOptions = ref ( [ ] )
const batchHostMigrateOptionsLoading = ref ( false )
const showBatchHostMigrateHostSelector = ref ( false )
const showBatchHostMigrateNetworkSelector = ref ( false )
const batchHostMigrateForm = reactive ( {
target _host _id : 0 ,
target _host _group _id : 0 ,
ipv4 _num : 0 ,
ipv6 _num : 0 ,
expire _offset _days : 0 ,
network _ids : [ ] ,
_hostName : ''
} )
const resetBatchHostMigrateForm = ( ) => {
Object . assign ( batchHostMigrateForm , {
target _host _id : 0 ,
target _host _group _id : 0 ,
ipv4 _num : 0 ,
ipv6 _num : 0 ,
expire _offset _days : 0 ,
network _ids : [ ] ,
_hostName : ''
} )
batchHostMigrateMode . value = 'host'
batchHostMigrateNetworks . value = [ ]
}
const openBatchHostMigrate = async ( ) => {
const targets = selectedVms . value . filter ( vm => ! vm . migrating )
if ( ! targets . length ) {
ElMessage . warning ( '所选虚拟机均正在迁移中,请重新选择' )
return
}
const skipped = selectedVms . value . length - targets . length
if ( skipped ) ElMessage . warning ( ` 已跳过 ${ skipped } 台正在迁移的虚拟机 ` )
batchHostMigrateTargets . value = [ ... targets ]
resetBatchHostMigrateForm ( )
batchHostMigrateVisible . value = true
batchHostMigrateOptionsLoading . value = true
try {
const res = await getRemoteHostGroupList ( { service _id : serviceId . value , page : 1 , page _size : 100 } )
if ( res ? . data ? . code === 200 && res ? . data ? . data ) {
const inner = res . data . data
batchHostMigrateGroupOptions . value = Array . isArray ( inner ) ? inner : ( inner . host _groups || inner . data || [ ] )
} else {
batchHostMigrateGroupOptions . value = [ ]
}
} catch {
batchHostMigrateGroupOptions . value = [ ]
} finally {
batchHostMigrateOptionsLoading . value = false
}
}
const handleBatchHostMigrateHostSelected = ( host ) => {
if ( ! host ? . id ) return
batchHostMigrateForm . target _host _id = Number ( host . id )
batchHostMigrateForm . _hostName = host . name || host . ip || ''
batchHostMigrateForm . network _ids = [ ]
batchHostMigrateNetworks . value = [ ]
}
const handleBatchHostMigrateNetworkSelected = ( networks ) => {
const list = Array . isArray ( networks ) ? networks : ( networks ? [ networks ] : [ ] )
const merged = [ ... batchHostMigrateNetworks . value , ... list ]
batchHostMigrateNetworks . value = [ ... new Map ( merged . filter ( item => item ? . id ) . map ( item => [ Number ( item . id ) , item ] ) ) . values ( ) ]
batchHostMigrateForm . network _ids = batchHostMigrateNetworks . value . map ( item => Number ( item . id ) )
}
const removeBatchHostMigrateNetwork = ( networkId ) => {
batchHostMigrateNetworks . value = batchHostMigrateNetworks . value . filter ( item => Number ( item . id ) !== Number ( networkId ) )
batchHostMigrateForm . network _ids = batchHostMigrateNetworks . value . map ( item => Number ( item . id ) )
}
const submitBatchHostMigrate = async ( ) => {
if ( batchHostMigrateMode . value === 'host' && ! batchHostMigrateForm . target _host _id ) {
ElMessage . warning ( '请选择目标宿主机' )
return
}
if ( batchHostMigrateMode . value === 'group' && ! batchHostMigrateForm . target _host _group _id ) {
ElMessage . warning ( '请选择目标宿主机组' )
return
}
if ( ! batchHostMigrateTargets . value . length ) {
ElMessage . warning ( '没有可迁移的虚拟机' )
return
}
const targetName = batchHostMigrateMode . value === 'host'
? ( batchHostMigrateForm . _hostName || batchHostMigrateForm . target _host _id )
: ( batchHostMigrateGroupOptions . value . find ( group => Number ( group . id ) === Number ( batchHostMigrateForm . target _host _group _id ) ) ? . name || batchHostMigrateForm . target _host _group _id )
try {
await ElMessageBox . confirm (
` 确定将 ${ batchHostMigrateTargets . value . length } 台虚拟机迁移到「 ${ targetName } 」吗? ` ,
'批量宿主机迁移' ,
{ confirmButtonText : '确定迁移' , cancelButtonText : '取消' , type : 'warning' }
)
} catch { return }
batchHostMigrateSubmitting . value = true
batchLoading . value = true
let success = 0
const failed = [ ]
for ( const vm of batchHostMigrateTargets . value ) {
try {
const formData = new FormData ( )
formData . append ( 'service_id' , serviceId . value )
formData . append ( 'vm_id' , vm . id )
if ( batchHostMigrateMode . value === 'host' ) formData . append ( 'target_host_id' , batchHostMigrateForm . target _host _id )
else formData . append ( 'target_host_group_id' , batchHostMigrateForm . target _host _group _id )
if ( batchHostMigrateForm . ipv4 _num > 0 ) formData . append ( 'ipv4_num' , batchHostMigrateForm . ipv4 _num )
if ( batchHostMigrateForm . ipv6 _num > 0 ) formData . append ( 'ipv6_num' , batchHostMigrateForm . ipv6 _num )
if ( batchHostMigrateForm . expire _offset _days > 0 ) formData . append ( 'expire_offset_days' , batchHostMigrateForm . expire _offset _days )
if ( batchHostMigrateMode . value === 'host' ) batchHostMigrateForm . network _ids . forEach ( id => formData . append ( 'network_ids' , id ) )
const res = await migrateVm ( formData )
if ( res ? . data ? . code === 200 ) success ++
else failed . push ( vm . name || ` ID ${ vm . id } ` )
} catch {
failed . push ( vm . name || ` ID ${ vm . id } ` )
}
}
batchHostMigrateSubmitting . value = false
batchLoading . value = false
batchHostMigrateVisible . value = false
clearSelection ( )
if ( failed . length ) {
ElMessage . warning ( ` 批量宿主机迁移提交完成:成功 ${ success } ,失败 ${ failed . length } ( ${ failed . slice ( 0 , 3 ) . join ( '、' ) } ${ failed . length > 3 ? ' 等' : '' } ) ` )
} else {
ElMessage . success ( ` 已提交 ${ success } 台虚拟机的宿主机迁移任务 ` )
}
loadList ( )
}
/ / B a t c h d a t a m i g r a t i o n
const batchMigrateVisible = ref ( false )
const batchMigrateSubmitting = ref ( false )
@@ -598,6 +790,7 @@ const batchMigrateForm = reactive({
target _host _id : 0 ,
ipv4 _num : 0 ,
ipv6 _num : 0 ,
expire _offset _days : 0 ,
network _ids : [ ] ,
_serviceName : '' ,
_hostName : ''
@@ -609,6 +802,7 @@ const resetBatchMigrateForm = () => {
target _host _id : 0 ,
ipv4 _num : 0 ,
ipv6 _num : 0 ,
expire _offset _days : 0 ,
network _ids : [ ] ,
_serviceName : '' ,
_hostName : ''
@@ -694,6 +888,7 @@ const submitBatchDataMigrate = async () => {
formData . append ( 'target_host_id' , batchMigrateForm . target _host _id )
if ( batchMigrateForm . ipv4 _num > 0 ) formData . append ( 'ipv4_num' , batchMigrateForm . ipv4 _num )
if ( batchMigrateForm . ipv6 _num > 0 ) formData . append ( 'ipv6_num' , batchMigrateForm . ipv6 _num )
if ( batchMigrateForm . expire _offset _days > 0 ) formData . append ( 'expire_offset_days' , batchMigrateForm . expire _offset _days )
batchMigrateForm . network _ids . forEach ( id => formData . append ( 'network_ids' , id ) )
const res = await dataMigrateVm ( formData )
if ( res ? . data ? . code === 200 ) success ++
@@ -1398,6 +1593,7 @@ defineExpose({ loadList })
. migrate - inline - pct { color : # e6a23c ; font - size : 12 px ; white - space : nowrap ; }
. batch - bar {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
gap : 10 px ;
padding : 10 px 16 px ;