Merge pull request 'fix: deduplicate expired token handling' (#35) from master into deploy
Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
+9
-1
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js'
|
||||
import AdminLayout from '../components/layout/AdminLayout.vue'
|
||||
import OperationLog from '@/views/system/OperationLog.vue'
|
||||
|
||||
@@ -809,7 +810,14 @@ router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
|
||||
|
||||
// 这里可以添加登录验证逻辑
|
||||
const isAuthenticated = localStorage.getItem('token')
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const expire = Number(localStorage.getItem(TOKEN_EXPIRE_KEY))
|
||||
const isAuthenticated = !!token && Number.isFinite(expire) && expire > 0 && Date.now() < expire * 1000
|
||||
if (token && !isAuthenticated) {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
}
|
||||
if (to.path !== '/login' && !isAuthenticated) {
|
||||
next({ path: '/login' })
|
||||
} else {
|
||||
|
||||
+28
-23
@@ -46,6 +46,22 @@ const isTokenExpiringSoon = () => {
|
||||
let isRefreshing = false
|
||||
// 等待刷新token的请求队列
|
||||
let refreshSubscribers = []
|
||||
let authRedirecting = false
|
||||
|
||||
const clearAuthAndRedirect = (message = '登录过期,请重新登录') => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
onTokenRefreshed(null)
|
||||
if (authRedirecting) return
|
||||
authRedirecting = true
|
||||
ElMessage.closeAll()
|
||||
ElMessage.warning(message)
|
||||
if (router.currentRoute.value.path !== '/login') router.replace('/login')
|
||||
}
|
||||
|
||||
// 鉴权失效后终止原页面请求链,避免并发请求进入各页面 catch 后重复弹错。
|
||||
const stopExpiredRequest = () => new Promise(() => {})
|
||||
|
||||
// 添加请求到队列
|
||||
const subscribeTokenRefresh = (callback) => {
|
||||
@@ -83,20 +99,12 @@ const doRefreshToken = async () => {
|
||||
}
|
||||
}
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
clearAuthAndRedirect()
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Token刷新失败:', error)
|
||||
// 刷新失败,触发登出逻辑
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
router.push('/login')
|
||||
clearAuthAndRedirect()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -122,6 +130,10 @@ class Request {
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
clearAuthAndRedirect()
|
||||
return stopExpiredRequest()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
@@ -206,14 +218,8 @@ http2.interceptors.request.use(async config => {
|
||||
if (urlNeedAuth(config.url)) {
|
||||
// 检查token是否已过期
|
||||
if (isTokenExpired()) {
|
||||
if (token) {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_EXPIRE_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
ElMessage.warning('登录过期,请重新登录')
|
||||
}
|
||||
router.push('/login')
|
||||
return Promise.reject(new Error('Token已过期'))
|
||||
clearAuthAndRedirect(token ? '登录过期,请重新登录' : '请先登录')
|
||||
return stopExpiredRequest()
|
||||
}
|
||||
|
||||
// 检查token是否即将过期,进行无感刷新
|
||||
@@ -251,6 +257,7 @@ http2.interceptors.request.use(async config => {
|
||||
})
|
||||
} else {
|
||||
// 正常情况,直接使用token
|
||||
authRedirecting = false
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
@@ -269,14 +276,12 @@ http2.interceptors.response.use(
|
||||
}
|
||||
const { status } = error.response;
|
||||
if (status === 401) {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
ElMessage.warning('登陆过期,请重新登陆')
|
||||
router.push('/login')
|
||||
return Promise.reject();
|
||||
clearAuthAndRedirect()
|
||||
return stopExpiredRequest()
|
||||
}
|
||||
// 其他错误处理(可选)
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default request
|
||||
export default request
|
||||
|
||||
@@ -889,6 +889,10 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="到期顺延">
|
||||
<el-input-number v-model="migrateForm.expire_offset_days" :min="0" :step="1" :precision="0" controls-position="right" style="width:100%" />
|
||||
<div style="font-size:12px;color:#909399">迁移完成后,用户商品到期时间向后顺延;0 表示不调整</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="migrateVisible = false">取消</el-button>
|
||||
@@ -2049,10 +2053,10 @@ 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 migrateForm = reactive({ target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, expire_offset_days: 0 })
|
||||
|
||||
const openMigrateVm = async () => {
|
||||
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, expire_offset_days: 0 })
|
||||
migrateMode.value = 'host'
|
||||
migrateVisible.value = true
|
||||
migrateOptionsLoading.value = true
|
||||
@@ -2097,6 +2101,7 @@ const submitMigrateVm = async () => {
|
||||
else payload.target_host_group_id = migrateForm.target_host_group_id
|
||||
if (migrateForm.ipv4_num) payload.ipv4_num = migrateForm.ipv4_num
|
||||
if (migrateForm.ipv6_num) payload.ipv6_num = migrateForm.ipv6_num
|
||||
if (migrateForm.expire_offset_days > 0) payload.expire_offset_days = migrateForm.expire_offset_days
|
||||
const res = await migrateUserVm(payload)
|
||||
if (res?.data?.code === 200) {
|
||||
const migrationData = res.data.data || {}
|
||||
|
||||
@@ -1219,6 +1219,10 @@
|
||||
<el-input-number v-model="migrateForm.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="migrateForm.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="目标网络" v-if="migrateMode === 'host'">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:100%">
|
||||
<el-tag v-for="n in migrateSelectedNetworks" :key="n.id" closable @close="removeMigrateNetwork(n.id)">{{ n.name || n.id }} (ID:{{ n.id }})</el-tag>
|
||||
@@ -1275,6 +1279,10 @@
|
||||
<el-input-number v-model="dataMigrateForm.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="dataMigrateForm.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="网络ID">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:100%">
|
||||
<el-tag v-for="n in dataMigrateSelectedNetworks" :key="n.id" closable @close="removeDataMigrateNetwork(n.id)">
|
||||
@@ -2691,7 +2699,7 @@ 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, network_ids: [] })
|
||||
const migrateForm = reactive({ target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [], expire_offset_days: 0 })
|
||||
const showMigrateNetworkSelector = ref(false)
|
||||
const migrateSelectedNetworks = ref([])
|
||||
const clearMigrateNetworks = () => { migrateSelectedNetworks.value = []; migrateForm.network_ids = [] }
|
||||
@@ -2704,7 +2712,7 @@ const handleMigrateNetworkConfirm = items => {
|
||||
const removeMigrateNetwork = id => { migrateSelectedNetworks.value = migrateSelectedNetworks.value.filter(item => item.id !== id); migrateForm.network_ids = migrateSelectedNetworks.value.map(item => item.id) }
|
||||
|
||||
const handleMigrateVm = async () => {
|
||||
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
|
||||
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [], expire_offset_days: 0 })
|
||||
migrateSelectedNetworks.value = []
|
||||
migrateMode.value = 'host'
|
||||
migrateDialogVisible.value = true
|
||||
@@ -2737,6 +2745,7 @@ const submitMigrateVm = async () => {
|
||||
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)
|
||||
if (migrateForm.expire_offset_days > 0) fd.append('expire_offset_days', migrateForm.expire_offset_days)
|
||||
migrateForm.network_ids.forEach(id => fd.append('network_ids', id))
|
||||
const res = await migrateVm(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
@@ -2762,7 +2771,7 @@ const dataMigrationId = ref('')
|
||||
const dataMigrateTaskId = ref('')
|
||||
const dataMigrationRecordId = ref(null)
|
||||
const dataMigrationChainId = ref('')
|
||||
const dataMigrateForm = reactive({ target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
|
||||
const dataMigrateForm = reactive({ target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [], expire_offset_days: 0 })
|
||||
const dataMigrateNetworkOptions = ref([])
|
||||
const showDataMigrateNetworkSelector = ref(false)
|
||||
const dataMigrateNetSelectorRef = ref(null)
|
||||
@@ -2786,7 +2795,7 @@ const removeDataMigrateNetwork = (id) => {
|
||||
}
|
||||
|
||||
const handleDataMigrateVm = async () => {
|
||||
Object.assign(dataMigrateForm, { target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [] })
|
||||
Object.assign(dataMigrateForm, { target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0, network_ids: [], expire_offset_days: 0 })
|
||||
dataMigrateSelectedNetworks.value = []
|
||||
dataMigrateNetworkOptions.value = []
|
||||
dataMigrateVisible.value = true
|
||||
@@ -2839,6 +2848,7 @@ const submitDataMigrate = async () => {
|
||||
fd.append('target_host_id', dataMigrateForm.target_host_id)
|
||||
if (dataMigrateForm.ipv4_num) fd.append('ipv4_num', dataMigrateForm.ipv4_num)
|
||||
if (dataMigrateForm.ipv6_num) fd.append('ipv6_num', dataMigrateForm.ipv6_num)
|
||||
if (dataMigrateForm.expire_offset_days > 0) fd.append('expire_offset_days', dataMigrateForm.expire_offset_days)
|
||||
if (dataMigrateForm.network_ids?.length) {
|
||||
dataMigrateForm.network_ids.forEach(id => fd.append('network_ids', id))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// Batch data migration
|
||||
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: 12px; white-space: nowrap; }
|
||||
.batch-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
|
||||
Reference in New Issue
Block a user