From 4bf7c4857b9bc9a1aa033ce4baca23fc85e1e3b3 Mon Sep 17 00:00:00 2001 From: shiran Date: Fri, 26 Jun 2026 16:46:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E4=BB=A3=E9=87=91=E5=88=B8=E7=AE=A1=E7=90=86=E4=B8=8E=E4=BC=98?= =?UTF-8?q?=E6=83=A0=E6=A8=A1=E5=9D=97=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/components/admin/NetworkSelectorPopup.vue | 8 +- src/views/marketing/VoucherHistory.vue | 23 +- src/views/marketing/VoucherHolders.vue | 16 +- src/views/product/ProductGroup.vue | 42 ++- src/views/product/UserGoodsList.vue | 17 +- .../product/components/ProductPlanManager.vue | 19 + src/views/user-vm/UserVmList.vue | 2 + src/views/user/UserDetail.vue | 310 ++++++++++++++++- src/views/user/UserGroup.vue | 324 +++++++++++++++--- src/views/virtualization/HostDetail.vue | 9 +- src/views/virtualization/ImageDetail.vue | 8 +- src/views/virtualization/NetworkManage.vue | 97 +++++- src/views/virtualization/RecycleBinManage.vue | 81 ++++- src/views/virtualization/VmDetail.vue | 9 +- src/views/virtualization/VolumeDetail.vue | 8 +- 15 files changed, 862 insertions(+), 111 deletions(-) diff --git a/src/components/admin/NetworkSelectorPopup.vue b/src/components/admin/NetworkSelectorPopup.vue index 1961bdc..c9d85da 100644 --- a/src/components/admin/NetworkSelectorPopup.vue +++ b/src/components/admin/NetworkSelectorPopup.vue @@ -36,13 +36,18 @@ - + + + +
{ const effectiveUsed = props.filterUsed || usedFilter.value if (effectiveUsed) params.used = effectiveUsed if (ipVersionFilter.value) params.ip_version = ipVersionFilter.value + params.disable = false const res = await getNetworkList(params) if (res?.data?.code === 200 && res?.data?.data) { const inner = res.data.data diff --git a/src/views/marketing/VoucherHistory.vue b/src/views/marketing/VoucherHistory.vue index 171fddb..98f4f1c 100644 --- a/src/views/marketing/VoucherHistory.vue +++ b/src/views/marketing/VoucherHistory.vue @@ -15,9 +15,6 @@
- - - 查询 @@ -203,15 +200,14 @@ const router = useRouter() // 查询参数 const queryParams = reactive({ user_id: undefined, - code_id: props.codeId || undefined, - id: '', + id: props.codeId || undefined, page: 1, count: 10 }) watch(() => props.codeId, (newVal) => { if (newVal) { - queryParams.code_id = newVal + queryParams.id = newVal fetchHistoryList() } }) @@ -285,8 +281,8 @@ const statistics = computed(() => { }) // 获取查询用户名称 const getQueryUserName = () => { - const user = UserOptions.value.find(u => u.UserId === queryParams.user_id) - return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}` + const user = UserOptions.value.find(u => u.user_id === queryParams.user_id) + return user ? `${user.user_name} (ID: ${user.user_id})` : `用户ID: ${queryParams.user_id}` } // 获取使用记录列表 @@ -347,15 +343,13 @@ const confirmUserSelection = (user) => { } if (selectorType.value === 'query') { - // 查询表单选择 - queryParams.user_id = user.UserId + queryParams.user_id = user.user_id } else { - // 编辑表单选择 - editForm.user_id = user.UserId + editForm.user_id = user.user_id } // 将选中的用户添加到 UserOptions 中(如果不存在) - if (!UserOptions.value.find(u => u.UserId === user.UserId)) { + if (!UserOptions.value.find(u => u.user_id === user.user_id)) { UserOptions.value.push(user) } @@ -372,8 +366,7 @@ const handleQuery = () => { // 重置查询 const resetQuery = () => { queryParams.user_id = undefined - queryParams.code_id = undefined - queryParams.id = '' + queryParams.id = undefined queryParams.page = 1 fetchHistoryList() } diff --git a/src/views/marketing/VoucherHolders.vue b/src/views/marketing/VoucherHolders.vue index e9bb181..5c9b7a6 100644 --- a/src/views/marketing/VoucherHolders.vue +++ b/src/views/marketing/VoucherHolders.vue @@ -423,15 +423,13 @@ const confirmUserSelection = (user) => { } if (selectorType.value === 'query') { - // 查询表单选择 - queryParams.user_id = user.UserId + queryParams.user_id = user.user_id } else { - // 编辑表单选择 - editForm.user_id = user.UserId + editForm.user_id = user.user_id } // 将选中的用户添加到 UserOptions 中(如果不存在) - if (!UserOptions.value.find(u => u.UserId === user.UserId)) { + if (!UserOptions.value.find(u => u.user_id === user.user_id)) { UserOptions.value.push(user) } @@ -451,14 +449,14 @@ const clearEditUser = () => { // 获取查询用户名称 const getQueryUserName = () => { - const user = UserOptions.value.find(u => u.UserId === queryParams.user_id) - return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}` + const user = UserOptions.value.find(u => u.user_id === queryParams.user_id) + return user ? `${user.user_name} (ID: ${user.user_id})` : `用户ID: ${queryParams.user_id}` } // 获取编辑用户名称 const getEditUserName = () => { - const user = UserOptions.value.find(u => u.UserId === editForm.user_id) - return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${editForm.user_id}` + const user = UserOptions.value.find(u => u.user_id === editForm.user_id) + return user ? `${user.user_name} (ID: ${user.user_id})` : `用户ID: ${editForm.user_id}` } // 获取代金券列表 diff --git a/src/views/product/ProductGroup.vue b/src/views/product/ProductGroup.vue index 6ef24cd..e8c08fd 100644 --- a/src/views/product/ProductGroup.vue +++ b/src/views/product/ProductGroup.vue @@ -122,6 +122,12 @@ 商品 {{ row.name }} + {{ row.isGroup ? row.recommendWords : row.data?.recommendWords }}
@@ -207,6 +213,7 @@
{{ row.name }} + {{ row.recommendWords }}
@@ -393,6 +400,11 @@ + + + + + + + + { cover_id: undefined, cover_url: '', tag_id: parentTagId || undefined, - index: 0 + index: 0, + recommend_words: '' }) console.log('添加子级,父级信息:', parentRow.name, 'ID:', parentRow.id, 'Level:', parentRow.level, '标签:', parentRow.tag) } else { @@ -1511,7 +1529,8 @@ const handleAdd = (parentRow) => { cover_id: undefined, cover_url: '', tag_id: undefined, - index: 0 + index: 0, + recommend_words: '' }) } @@ -1532,7 +1551,8 @@ const handleEdit = (row) => { cover_id: row.coverId || undefined, cover_url: row.cover || '', tag_id: row.tag?.id || row.tagId || undefined, - index: row.index || 0 + index: row.index || 0, + recommend_words: row.recommendWords || '' }) dialogVisible.value = true @@ -1602,7 +1622,8 @@ const handleAddProduct = () => { arg_type: 'all', require_real_name: false, max_per_user: 0, - max_first_purchase_duration: 0 + max_first_purchase_duration: 0, + recommend_words: '' }) selectedProductGroup.value = null @@ -1642,7 +1663,8 @@ const handleEditProduct = (product, parentGroupId) => { max_first_purchase_duration: product.maxFirstPurchaseDuration ?? product.max_first_purchase_duration ?? 0, send_notice: !!product.sendNotice, renew_price: (product.renewPrice ?? product.renew_price ?? 0) / 100, - renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0 + renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0, + recommend_words: product.recommendWords ?? product.recommend_words ?? '' }) productDialogVisible.value = true @@ -1674,7 +1696,8 @@ const submitProductForm = () => { max_first_purchase_duration: Number(productForm.max_first_purchase_duration) || 0, send_notice: productForm.send_notice === true, renew_price: Number(productForm.renew_price) || 0, - renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0 + renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0, + recommend_words: productForm.recommend_words || '' } let res @@ -1813,7 +1836,8 @@ const submitForm = () => { name: groupForm.name.trim(), note: groupForm.note || '', disable: groupForm.disable, - index: Number(groupForm.index) || 0 + index: Number(groupForm.index) || 0, + recommend_words: groupForm.recommend_words || '' } if (groupForm.parent_id) { diff --git a/src/views/product/UserGoodsList.vue b/src/views/product/UserGoodsList.vue index 016bb3e..44cb504 100644 --- a/src/views/product/UserGoodsList.vue +++ b/src/views/product/UserGoodsList.vue @@ -24,6 +24,13 @@
{{ counts.expired }}
+
+
+
+
待开通
+
{{ counts.pending }}
+
+
@@ -60,6 +67,7 @@ + @@ -133,6 +141,7 @@ 已删除 已到期 + 待开通 正常 @@ -513,7 +522,7 @@ import { ref, reactive, computed, onMounted, watch } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Refresh, Search, ArrowDown, Box, CircleCheck, Clock, Delete } from '@element-plus/icons-vue' +import { Plus, Refresh, Search, ArrowDown, Box, CircleCheck, Clock, Delete, Timer } from '@element-plus/icons-vue' import { getUserGoodsList, getUserGoodsCount, createUserGoods, updateUserGoods, deleteUserGoods, getUserVmList, getExpireRemindList, sendExpireRemind } from '@/api/admin/userVm' import { extractApiError } from '@/utils/kvmErrorUtil' import { formatToApiTime } from '@/utils/tool' @@ -536,8 +545,8 @@ const showFilterUserSelector = ref(false) const showFilterProductSelector = ref(false) // 用户商品数量统计 -const counts = reactive({ normal: 0, deleted: 0, expired: 0 }) -const countTotal = computed(() => (counts.normal || 0) + (counts.deleted || 0) + (counts.expired || 0)) +const counts = reactive({ normal: 0, deleted: 0, expired: 0, pending: 0 }) +const countTotal = computed(() => (counts.normal || 0) + (counts.deleted || 0) + (counts.expired || 0) + (counts.pending || 0)) const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-' @@ -616,6 +625,7 @@ const loadCount = async () => { counts.normal = d.normal ?? 0 counts.deleted = d.deleted ?? 0 counts.expired = d.expired ?? 0 + counts.pending = d.pending ?? 0 } } catch { /* 统计失败不阻断列表 */ } } @@ -1054,6 +1064,7 @@ onMounted(() => { loadList(); loadCount() }) .stat-icon-normal { background: #f0faf0; color: #52c41a; } .stat-icon-expired { background: #fff7e6; color: #fa8c16; } .stat-icon-deleted { background: #fff1f0; color: #f5222d; } +.stat-icon-pending { background: #e6f7ff; color: #1890ff; } .stat-body { display: flex; flex-direction: column; } .stat-label { font-size: 13px; color: #909399; margin-bottom: 2px; } diff --git a/src/views/product/components/ProductPlanManager.vue b/src/views/product/components/ProductPlanManager.vue index 6963eac..2228927 100644 --- a/src/views/product/components/ProductPlanManager.vue +++ b/src/views/product/components/ProductPlanManager.vue @@ -47,6 +47,9 @@ ¥ {{ formatPlanPrice(row) }}
+
+ 续费 ¥{{ formatRenewPrice(row) }} +
@@ -471,6 +474,14 @@ const formatPlanPrice = (row) => { return (Number(raw) / 100).toFixed(2) } +const getRenewPrice = (row) => { + return Number(row.renewFixedPrice ?? row.renew_fixed_price ?? 0) +} + +const formatRenewPrice = (row) => { + return (getRenewPrice(row) / 100).toFixed(2) +} + const getInventoryNum = (row) => { return Number(row.inventory ?? 0) || 0 } @@ -1018,6 +1029,14 @@ watch(() => props.visible, (val) => { color: #f56c6c; font-weight: 700; } +.plan-renew-price { + font-size: 11px; + color: #e6a23c; + font-weight: 500; + margin-top: 2px; + line-height: 1; +} + .plan-stat-price.is-dynamic { background: linear-gradient(135deg, #f0f7ff 0%, #e6f2ff 100%); border-color: #c6e2ff; diff --git a/src/views/user-vm/UserVmList.vue b/src/views/user-vm/UserVmList.vue index 214c790..05fce08 100644 --- a/src/views/user-vm/UserVmList.vue +++ b/src/views/user-vm/UserVmList.vue @@ -33,6 +33,7 @@ + @@ -86,6 +87,7 @@ 已删除 + 待开通 正常 diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue index 6c44583..f20c9df 100644 --- a/src/views/user/UserDetail.vue +++ b/src/views/user/UserDetail.vue @@ -294,6 +294,64 @@
+ +
+ + 添加代金券 + + + 刷新 + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
@@ -496,6 +554,49 @@ + + + + + + + + + + + + + + + + + + + + + @@ -536,7 +637,7 @@ import UserListSelector from '@/components/admin/UserListSelector.vue' import { ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock, UserFilled, Document, Clock, List, Switch, User, Camera, Upload, - UploadFilled, Key, Monitor, Setting, Close + UploadFilled, Key, Monitor, Setting, Close, Plus } from '@element-plus/icons-vue' import { getUserGroupList, getUserBalanceCount } from '@/api/admin/user' import { getFileDetail } from '@/api/admin/file' @@ -549,6 +650,13 @@ import { getAdminGroupList } from '@/api/admin/group' import { getOrderList } from '@/api/admin/order' import { getTickerList } from '@/api/ticket' import { getUserGoodsList } from '@/api/admin/product' +import { + getUserVoucherList, + allocateVoucher, + updateUserVoucher, + deleteUserVoucher, + getDiscountCodeList +} from '@/api/admin/discount' const Edit = EditIcon const route = useRoute() @@ -610,6 +718,27 @@ const goodsListTotal = ref(0) const goodsListPage = ref(1) const goodsListPageSize = ref(10) +// 代金券列表相关 +const userVoucherList = ref([]) +const voucherListLoading = ref(false) +const voucherListTotal = ref(0) +const voucherListPage = ref(1) +const voucherListPageSize = ref(10) +const voucherDialogVisible = ref(false) +const voucherDialogType = ref('add') +const voucherFormRef = ref(null) +const discountCouponOptions = ref([]) +const voucherForm = reactive({ + discount_id: undefined, + id: undefined, + use_times: 0, + max_use_times: 0, + expire_at: undefined +}) +const voucherFormRules = { + discount_id: [{ required: true, message: '请选择代金券', trigger: 'change' }] +} + // 用户余额相关 const userBalance = ref({ balance: 0, @@ -734,6 +863,8 @@ const handleTabClick = (tab) => { fetchUserTicketList() } else if (tab.props.name === '5') { fetchUserGoodsList() + } else if (tab.props.name === '6') { + fetchUserVoucherListData() } }; @@ -1126,6 +1257,169 @@ const handleGoodsListPageChange = (page) => { fetchUserGoodsList() } +// 获取用户代金券列表 +const fetchUserVoucherListData = async () => { + if (!route.query.user_id) return + voucherListLoading.value = true + try { + const res = await getUserVoucherList({ + user_id: route.query.user_id, + page: voucherListPage.value, + count: voucherListPageSize.value + }) + if (res.data.code === 200) { + userVoucherList.value = res.data.data?.data || [] + voucherListTotal.value = res.data.data?.all_count || 0 + } + } catch (error) { + ElMessage.error('获取代金券列表失败') + } finally { + voucherListLoading.value = false + } +} + +const handleVoucherListSizeChange = (size) => { + voucherListPageSize.value = size + voucherListPage.value = 1 + fetchUserVoucherListData() +} + +const handleVoucherListPageChange = (page) => { + voucherListPage.value = page + fetchUserVoucherListData() +} + +// 代金券状态判断 +const getVoucherStatusText = (row) => { + if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) return '已用完' + const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0 + if (expireAt > 0 && new Date(row.expireAt).getFullYear() !== 1970 && expireAt < Date.now()) return '已过期' + return '可使用' +} + +const getVoucherStatusType = (row) => { + if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) return 'info' + const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0 + if (expireAt > 0 && new Date(row.expireAt).getFullYear() !== 1970 && expireAt < Date.now()) return 'warning' + return 'success' +} + +// 格式化代金券日期(过滤零值日期) +const formatVoucherDate = (dateStr) => { + if (!dateStr) return '-' + const date = new Date(dateStr) + if (isNaN(date.getTime()) || date.getFullYear() === 1970 || date.getFullYear() === 1) return '-' + return formatDate(dateStr) +} + +// 获取代金券选项(用于添加对话框) +const fetchDiscountCouponOptions = async () => { + try { + const res = await getDiscountCodeList({ discount_type: 'coupon', page: 1, count: 100 }) + if (res.data.code === 200) { + discountCouponOptions.value = res.data.data?.data || [] + } + } catch (error) { + console.error('获取代金券选项失败:', error) + } +} + +// 添加代金券 +const handleAddVoucher = () => { + voucherDialogType.value = 'add' + voucherDialogVisible.value = true + Object.assign(voucherForm, { + discount_id: undefined, + id: undefined, + use_times: 0, + max_use_times: 0, + expire_at: undefined + }) + voucherFormRef.value?.resetFields() + fetchDiscountCouponOptions() +} + +// 编辑代金券 +const handleEditVoucher = (row) => { + voucherDialogType.value = 'edit' + voucherDialogVisible.value = true + + let expireTime = undefined + if (row.expireAt && row.expireAt !== '0001-01-01T00:00:00Z') { + const date = new Date(row.expireAt) + if (!isNaN(date.getTime()) && date.getFullYear() !== 1970) { + expireTime = Math.floor(date.getTime() / 1000) + } + } + + Object.assign(voucherForm, { + discount_id: row.discountId, + id: row.Id, + use_times: row.useTimes || 0, + max_use_times: row.maxUseTimes || 0, + expire_at: expireTime + }) +} + +// 删除代金券 +const handleDeleteVoucher = (row) => { + const name = row.discount?.name || '该代金券' + ElMessageBox.confirm(`确认删除代金券「${name}」吗?`, '警告', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(async () => { + try { + const res = await deleteUserVoucher({ + user_id: route.query.user_id, + id: row.Id + }) + if (res.data.code === 200) { + ElMessage.success('删除成功') + fetchUserVoucherListData() + } else { + ElMessage.error(res.data.message || '删除失败') + } + } catch (error) { + ElMessage.error(error.response?.data?.message || '删除失败') + } + }).catch(() => {}) +} + +// 提交代金券表单 +const submitVoucherForm = () => { + voucherFormRef.value?.validate(async (valid) => { + if (!valid) return + try { + let res + if (voucherDialogType.value === 'add') { + res = await allocateVoucher({ + user_id: route.query.user_id, + code_id: voucherForm.discount_id + }) + } else { + res = await updateUserVoucher({ + user_id: route.query.user_id, + id: voucherForm.id, + discount_id: voucherForm.discount_id, + use_times: voucherForm.use_times, + max_use_times: voucherForm.max_use_times, + expire_at: voucherForm.expire_at + }) + } + if (res.data.code === 200) { + ElMessage.success(voucherDialogType.value === 'add' ? '添加成功' : '修改成功') + voucherDialogVisible.value = false + fetchUserVoucherListData() + } else { + ElMessage.error(res.data.message || '操作失败') + } + } catch (error) { + ElMessage.error(error.response?.data?.message || '操作失败') + } + }) +} + // 获取用户余额信息 const fetchUserBalance = async () => { if (!route.query.user_id) return @@ -1450,6 +1744,7 @@ const fetchActiveTabData = () => { else if (tab === '3') fetchUserOrderList() else if (tab === '4') fetchUserTicketList() else if (tab === '5') fetchUserGoodsList() + else if (tab === '6') fetchUserVoucherListData() } const loadUserData = async () => { @@ -1868,4 +2163,17 @@ onActivated(() => { min-width: 60px; } } + +/* 代金券操作栏 */ +.voucher-action-bar { + display: flex; + gap: 12px; + margin-bottom: 16px; +} + +.voucher-amount { + color: #f56c6c; + font-weight: bold; + font-size: 14px; +} diff --git a/src/views/user/UserGroup.vue b/src/views/user/UserGroup.vue index 6d40190..368991d 100644 --- a/src/views/user/UserGroup.vue +++ b/src/views/user/UserGroup.vue @@ -317,10 +317,10 @@ + - - - 商品 - 商品组 - - - - - - - - - - - + + +
+
+ 勾选商品组或商品(仅可选一个) +
+ 已选: {{ discountTreeChecked.name || '无' }} +
+
+ + + +
+ + + +
@@ -411,7 +463,7 @@ diff --git a/src/views/virtualization/NetworkManage.vue b/src/views/virtualization/NetworkManage.vue index 9eb69e9..0992634 100644 --- a/src/views/virtualization/NetworkManage.vue +++ b/src/views/virtualization/NetworkManage.vue @@ -33,6 +33,14 @@ + + + + + + + +
@@ -54,10 +62,34 @@ - + + + + + + + + + + @@ -113,6 +145,10 @@ + + +
禁用后该网络不参与自动分配
+
@@ -128,6 +156,7 @@ import { restoreRecycleBin, deleteRecycleBin, cleanRecycleBin } from '@/api/admin/kvmService' import { extractApiError } from '@/utils/kvmErrorUtil' +import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue' const serviceId = inject('serviceId') const hostId = inject('hostId') @@ -143,21 +172,25 @@ const filterStatus = ref('') const statusOptions = [ { value: 'pending', label: '等待归档' }, { value: 'archiving', label: '归档中' }, + { value: 'ready', label: '就绪' }, { value: 'archived', label: '已归档' }, { value: 'restoring', label: '恢复中' }, { value: 'purging', label: '清理中' } ] const statusLabelMap = { - pending: '等待归档', archiving: '归档中', archived: '已归档', + pending: '等待归档', archiving: '归档中', ready: '就绪', archived: '已归档', restoring: '恢复中', purging: '清理中', failed: '失败', error: '错误' } const statusLabel = (s) => statusLabelMap[s] || s || '-' const statusTagType = (s) => ({ - archived: 'success', pending: 'info', archiving: 'warning', + ready: 'success', archived: 'success', pending: 'info', archiving: 'warning', restoring: 'primary', purging: 'danger', failed: 'danger', error: 'danger' }[s] || 'info') +// ready 和 archived 都表示归档完成,可执行恢复 +const canRestore = (status) => status === 'ready' || status === 'archived' + const formatTs = (ts) => { if (!ts) return '-' if (typeof ts === 'object' && ts.seconds) return new Date(Number(ts.seconds) * 1000).toLocaleString('zh-CN') @@ -236,14 +269,30 @@ const handleDetail = async (row) => { const restoreVisible = ref(false) const restoreLoading = ref(false) const restoreRow = ref(null) -const restoreForm = reactive({ network_ids: '', internet_network_id: '' }) + +// 网络选择器 +const showBridgeNetSelector = ref(false) +const showNatNetSelector = ref(false) +const selectedBridgeNetwork = ref(null) +const selectedNatNetworks = ref([]) const handleRestore = (row) => { restoreRow.value = row - Object.assign(restoreForm, { network_ids: '', internet_network_id: '' }) + selectedBridgeNetwork.value = null + selectedNatNetworks.value = [] restoreVisible.value = true } +const handleBridgeNetConfirm = (net) => { selectedBridgeNetwork.value = net } +const handleNatNetConfirm = (net) => { + if (!selectedNatNetworks.value.find(n => n.id === net.id)) { + selectedNatNetworks.value.push(net) + } +} +const removeNatNetwork = (id) => { + selectedNatNetworks.value = selectedNatNetworks.value.filter(n => n.id !== id) +} + const submitRestore = async () => { restoreLoading.value = true try { @@ -251,13 +300,11 @@ const submitRestore = async () => { fd.append('service_id', serviceId.value) fd.append('host_id', hostId.value) fd.append('recycle_id', restoreRow.value.id) - if (restoreForm.network_ids) { - restoreForm.network_ids.split(',').map(s => s.trim()).filter(Boolean).forEach(id => { - fd.append('network_ids', id) - }) + if (selectedNatNetworks.value.length) { + selectedNatNetworks.value.forEach(n => fd.append('network_ids', n.id)) } - if (restoreForm.internet_network_id) { - fd.append('internet_network_id', restoreForm.internet_network_id) + if (selectedBridgeNetwork.value) { + fd.append('internet_network_id', selectedBridgeNetwork.value.id) } const res = await restoreRecycleBin(fd) if (res?.data?.code === 200) { @@ -322,4 +369,6 @@ defineExpose({ loadList }) .mono-text { font-family: Consolas, Monaco, monospace; font-size: 12px; } .snapshot-title { font-size: 13px; font-weight: 600; color: #606266; margin: 12px 0 6px; } .pagination-wrapper { margin-top: 12px; display: flex; justify-content: flex-end; } +.net-select-area { display: flex; flex-direction: column; gap: 6px; } +.net-selected-list { display: flex; flex-wrap: wrap; gap: 4px; } diff --git a/src/views/virtualization/VmDetail.vue b/src/views/virtualization/VmDetail.vue index 18ca704..48b5760 100644 --- a/src/views/virtualization/VmDetail.vue +++ b/src/views/virtualization/VmDetail.vue @@ -3956,11 +3956,14 @@ const loadSgLockInfo = async () => { } } -watch(vmId, () => { if (isPageActive) initPage() }) +const isCurrentRoute = () => route.name === 'VirtVmDetail' + +watch(vmId, () => { if (isPageActive && isCurrentRoute()) initPage() }) watch(activeTab, (tab) => { if (detail.value) triggerTabLoad(tab) }) -onActivated(() => { +onActivated(async () => { isPageActive = true - if (loadedVmId !== vmId.value) initPage() + await nextTick() + if (isCurrentRoute() && loadedVmId !== vmId.value) initPage() }) onDeactivated(() => { isPageActive = false; stopMigratePolling() }) onBeforeUnmount(() => { isPageActive = false; disposeCharts(); stopMigratePolling(); stopDetailAutoRefresh() }) diff --git a/src/views/virtualization/VolumeDetail.vue b/src/views/virtualization/VolumeDetail.vue index cb1285b..690bd97 100644 --- a/src/views/virtualization/VolumeDetail.vue +++ b/src/views/virtualization/VolumeDetail.vue @@ -110,7 +110,7 @@