Merge pull request 'fix: deduplicate expired token handling' (#35) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m59s
Build and Deploy Vue3 / deploy (push) Successful in 53s

Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
2026-08-08 17:31:34 +08:00
5 changed files with 255 additions and 31 deletions
+9 -1
View File
@@ -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
View File
@@ -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
+7 -2
View File
@@ -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 || {}
+14 -4
View File
@@ -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))
}
+197 -1
View File
@@ -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;