From 0155715278a7c77f3ecce5d8d9d571c9933cd0e1 Mon Sep 17 00:00:00 2001 From: shiran Date: Tue, 30 Jun 2026 10:26:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E8=99=9A=E6=8B=9F=E6=9C=BA=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E7=AE=A1=E7=90=86=E6=94=B9=E4=B8=BA=E8=A7=A3=E7=BB=91?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=BA=95=E5=B1=82=E7=BD=91=E7=BB=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 user-vm/UserVmDetail 和 virtualization/VmDetail 中的网络删除操作改为通过 updateVm 接口解绑,与绑定网络逻辑对称,避免误删底层物理网络。 Co-authored-by: Cursor --- src/views/user-vm/UserVmDetail.vue | 30 ++++++++++++++++----------- src/views/user/UserDetail.vue | 11 +++++++++- src/views/virtualization/VmDetail.vue | 26 +++++++++++++++++------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue index 3c351e9..086f220 100644 --- a/src/views/user-vm/UserVmDetail.vue +++ b/src/views/user-vm/UserVmDetail.vue @@ -344,7 +344,7 @@ @@ -1807,21 +1807,27 @@ const resolveNetworkServiceHost = async (row) => { } const handleDeleteVmNetwork = (row) => { ElMessageBox.confirm( - `将删除底层网络「${row.name}」(ID:${row.id}),该操作会影响所有绑定该网络的虚拟机,是否继续?`, - '删除网络', - { confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' } + `确定从该虚拟机解绑网络「${row.name}」(ID:${row.id}) 吗?`, + '解绑网络', + { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ).then(async () => { deletingNetworkId.value = row.id try { - const { serviceId, hostId } = await resolveNetworkServiceHost(row) - if (!serviceId) { ElMessage.error('无法获取该网络所属服务ID,删除失败'); return } - const params = { service_id: serviceId, network_id: row.id } - if (hostId) params.host_id = hostId - const res = await deletePointNetwork(params) - if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadDetail() } - else ElMessage.error(extractApiError(res?.data, '删除失败')) + const payload = { user_goods_id: userGoodsId.value } + const remainingNetworks = vmNetworks.value.filter(n => n.id !== row.id) + const bridgeIds = remainingNetworks.filter(n => n.type === 'bridge').map(n => n.id) + const natNet = remainingNetworks.find(n => n.type === 'nat') + payload.network_ids = bridgeIds + if (natNet) payload.internet_network_id = natNet.id + if (vm.value?.rx_bandwidth) payload.rx_bandwidth = vm.value.rx_bandwidth + if (vm.value?.tx_bandwidth) payload.tx_bandwidth = vm.value.tx_bandwidth + if (vm.value?.ssh_port) payload.ssh_port = vm.value.ssh_port + if (inPortGroup.value?.id) payload.port_group_id = inPortGroup.value.id + const res = await updateUserVm(payload) + if (res?.data?.code === 200) { ElMessage.success('解绑网络成功'); loadDetail() } + else ElMessage.error(extractApiError(res?.data, '解绑网络失败')) } catch (e) { - ElMessage.error(extractApiError(e?.response?.data, '删除失败')) + ElMessage.error(extractApiError(e?.response?.data, '解绑网络失败')) } finally { deletingNetworkId.value = 0 } diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue index f20c9df..efdd312 100644 --- a/src/views/user/UserDetail.vue +++ b/src/views/user/UserDetail.vue @@ -257,7 +257,11 @@ - + + + @@ -280,6 +284,11 @@ + + +
详情 编辑 设为主IP - 删除 + 解绑 @@ -2962,13 +2962,27 @@ const submitNetForm = () => { } const handleNetDetail = (row) => { netDetailData.value = row; netDetailVisible.value = true } const handleNetDelete = (row) => { - ElMessageBox.confirm(`确定要删除网络「${row.name}」(ID: ${row.id}) 吗?`, '删除网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + ElMessageBox.confirm(`确定从该虚拟机解绑网络「${row.name}」(ID: ${row.id}) 吗?`, '解绑网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) .then(async () => { + actionLoading.value = true try { - const res = await deleteNetwork({ service_id: serviceId.value, network_id: row.id, host_id: row.host_id }) - if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadDetail() } - else ElMessage.error(extractApiError(res?.data, '删除失败')) - } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) } + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('vm_id', vmId.value) + const remainingNetworks = vmNetworks.value.filter(n => n.id !== row.id) + const bridgeIds = remainingNetworks.filter(n => n.type === 'bridge').map(n => n.id) + const natNet = remainingNetworks.find(n => n.type === 'nat') + bridgeIds.forEach(id => fd.append('network_ids', id)) + if (natNet) fd.append('internet_network_id', natNet.id) + if (detail.value?.rx_bandwidth) fd.append('rx_bandwidth', detail.value.rx_bandwidth) + if (detail.value?.tx_bandwidth) fd.append('tx_bandwidth', detail.value.tx_bandwidth) + if (detail.value?.ssh_port) fd.append('ssh_port', detail.value.ssh_port) + if (vmPortGroup.value?.id) fd.append('port_group_id', vmPortGroup.value.id) + const res = await updateVm(fd) + if (res?.data?.code === 200) { ElMessage.success('解绑网络成功'); loadDetail() } + else ElMessage.error(extractApiError(res?.data, '解绑网络失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑网络失败')) } + finally { actionLoading.value = false } }).catch(() => {}) } -- 2.54.0 From 2e3574a182171bd14a6245e89228a2310fb50fa0 Mon Sep 17 00:00:00 2001 From: shiran Date: Tue, 7 Jul 2026 18:00:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=BB=A3=E7=90=86=E5=95=86=E6=9F=A5=E8=AF=A2=E3=80=81?= =?UTF-8?q?=E5=95=86=E5=93=81=E7=BB=AD=E8=B4=B9=E3=80=81VNC=E8=8A=82?= =?UTF-8?q?=E7=82=B9access=5Fmode=E3=80=81IDC=E4=BF=A1=E6=81=AF=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E3=80=81=E6=96=B0=E8=B4=AD=E8=AE=A2=E5=8D=95table?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户列表新增通过IDC代理商uid查询用户信息 - 用户商品详情新增续费功能(admin/good/user_goods/renew) - VNC节点管理新增access_mode和use_ssl参数 - 用户详情左栏新增IDC平台信息展示 - 新购订单选择商品后所属表不再被覆盖,固定为good Co-authored-by: Cursor --- src/api/admin/user.js | 6 ++ src/api/admin/userVm.js | 1 + src/views/order/OrderList.vue | 2 +- src/views/product/UserGoodsDetail.vue | 69 +++++++++++++++++++++- src/views/user/UserDetail.vue | 14 +++++ src/views/user/UserList.vue | 47 ++++++++++++++- src/views/virtualization/VncNodeManage.vue | 36 +++++++++-- 7 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/api/admin/user.js b/src/api/admin/user.js index 62fcb46..0fd469a 100644 --- a/src/api/admin/user.js +++ b/src/api/admin/user.js @@ -105,6 +105,12 @@ export const getUserOperationRecord = (data) => { export const mockUserLogin = (data) => { return http2.get('/api/v1/admin/user/user/simulation_login?user_id='+data.user_id) } + +/**通过IDC代理商uid获取用户信息 */ +export const getUserByIdcAgentUid = (params) => { + return http2.get('/api/v1/admin/user/idc_agent/user', { params }) +} + /**新建任务 */ export const createTask = (data) => { return http2.post('/api/v1/admin/user/user/create', data,{ diff --git a/src/api/admin/userVm.js b/src/api/admin/userVm.js index 908ab33..5527691 100644 --- a/src/api/admin/userVm.js +++ b/src/api/admin/userVm.js @@ -109,6 +109,7 @@ export const getUserGoodsDetail = (params) => http2.get(`${GOODS_BASE}/detail`, export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params }) +export const renewUserGoods = (data) => http2.post(`${GOODS_BASE}/renew`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } }) export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params }) export const getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { params }) diff --git a/src/views/order/OrderList.vue b/src/views/order/OrderList.vue index d552766..54ff4b8 100644 --- a/src/views/order/OrderList.vue +++ b/src/views/order/OrderList.vue @@ -1285,7 +1285,7 @@ const clearUser = () => { const handleProductSelect = (product) => { orderForm.commodity_id = product.id selectedProductInfo.value = product - if (product.table) orderForm.table = product.table + if (orderForm.type !== 'create' && product.table) orderForm.table = product.table orderForm.plan_id = null planFixedArgs.value = [] // 自动生成订单名称:商品名-时间戳 diff --git a/src/views/product/UserGoodsDetail.vue b/src/views/product/UserGoodsDetail.vue index b4e5b61..e396ce8 100644 --- a/src/views/product/UserGoodsDetail.vue +++ b/src/views/product/UserGoodsDetail.vue @@ -13,6 +13,9 @@ 刷新 + + 续费 + 编辑 @@ -233,6 +236,35 @@
+ + + + + + + +
+ + +
+
不填则使用商品默认续费价格
+
+ + ¥{{ renewTotalPrice }} + + + + + + + +
+ +
+ @@ -289,7 +321,7 @@ import { ref, reactive, computed, onMounted, onActivated, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { ArrowLeft, ArrowDown, Refresh, Edit, Delete } from '@element-plus/icons-vue' -import { getUserGoodsDetail, updateUserGoods, deleteUserGoods } from '@/api/admin/userVm' +import { getUserGoodsDetail, updateUserGoods, deleteUserGoods, renewUserGoods } from '@/api/admin/userVm' import { extractApiError } from '@/utils/kvmErrorUtil' import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue' import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue' @@ -417,6 +449,41 @@ const goToOrder = () => { router.push({ path: '/order/list', query: { order_id: oid } }) } +// ---- 续费 ---- +const renewVisible = ref(false) +const renewLoading = ref(false) +const renewForm = reactive({ purchase_duration: 1, renew_price: 0, complete_pay: false, deduct_balance: false }) +const renewTotalPrice = computed(() => { + const price = renewForm.renew_price || (detail.value?.renewPrice || detail.value?.renew_price || 0) / 100 + return (price * renewForm.purchase_duration).toFixed(2) +}) +const openRenew = () => { + const rawRenew = detail.value?.renewPrice || detail.value?.renew_price || 0 + Object.assign(renewForm, { purchase_duration: 1, renew_price: rawRenew / 100, complete_pay: false, deduct_balance: false }) + renewVisible.value = true +} +const handleRenew = async () => { + renewLoading.value = true + try { + const data = { id: goodsId.value, purchase_duration: renewForm.purchase_duration } + if (renewForm.renew_price) data.renew_price = Math.round(renewForm.renew_price * 100) + if (renewForm.complete_pay) data.complete_pay = true + if (renewForm.complete_pay && renewForm.deduct_balance) data.deduct_balance = true + const res = await renewUserGoods(data) + if (res?.data?.code === 200) { + ElMessage.success('续费成功') + renewVisible.value = false + loadDetail() + } else { + ElMessage.error(extractApiError(res?.data, '续费失败')) + } + } catch (e) { + ElMessage.error(extractApiError(e?.response?.data, '续费失败')) + } finally { + renewLoading.value = false + } +} + const editVisible = ref(false) const editForm = reactive({ note: '', renew_price: 0, base_price: 0, expire_time: '', item_id: 0, _serviceId: 0, _serviceName: '', _itemName: '' }) const showVmSelector = ref(false) diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue index efdd312..73fc0e0 100644 --- a/src/views/user/UserDetail.vue +++ b/src/views/user/UserDetail.vue @@ -116,6 +116,20 @@ {{ getRealNameTypeText(userInfo.RealName?.Type) }} + + + + + {{ userInfo.idc_agent.Uid || '-' }} + {{ userInfo.idc_agent.AgentLevelName || '-' }} + {{ userInfo.idc_agent.AgentLevent ?? '-' }} + {{ formatDate(userInfo.idc_agent.CreatedAt) }} + + diff --git a/src/views/user/UserList.vue b/src/views/user/UserList.vue index 0ce702f..5d9a616 100644 --- a/src/views/user/UserList.vue +++ b/src/views/user/UserList.vue @@ -32,10 +32,23 @@ @keyup.enter="handleJumpToUser" /> +
+ 代理商ID + +
跳转 + + 代理商查询 + 查询 @@ -653,7 +666,8 @@ import { updateUserGroup, updateUserRealName, getUserLoginRecord, - getUserOperationRecord + getUserOperationRecord, + getUserByIdcAgentUid } from '@/api/admin/user' import { getFileDetail } from '@/api/admin/file' import { closeAllMessage } from '../../utils/message' @@ -663,6 +677,10 @@ const router = useRouter() // 跳转用户ID const jumpUserId = ref('') +// 代理商ID查询 +const agentUid = ref('') +const agentSearchLoading = ref(false) + // 查询参数 const queryParams = reactive({ key: '', @@ -887,6 +905,33 @@ const handleJumpToUser = () => { }) } +// 通过代理商ID查询用户 +const handleSearchByAgentUid = async () => { + if (!agentUid.value || !agentUid.value.trim()) { + ElMessage.warning('请输入代理商ID') + return + } + agentSearchLoading.value = true + try { + const res = await getUserByIdcAgentUid({ uid: agentUid.value.trim() }) + if (res?.data?.code === 200 && res?.data?.data) { + const userId = res.data.data.UserId + if (userId) { + router.push({ path: '/user/detail', query: { user_id: userId } }) + } else { + ElMessage.warning('未找到绑定该代理商ID的用户') + } + } else { + ElMessage.warning(res?.data?.message || '未找到绑定该代理商ID的用户') + } + } catch (e) { + const msg = e?.response?.data?.message || '查询失败' + ElMessage.error(msg) + } finally { + agentSearchLoading.value = false + } +} + // 选择项变化 const handleSelectionChange = (selection) => { selectedRows.value = selection diff --git a/src/views/virtualization/VncNodeManage.vue b/src/views/virtualization/VncNodeManage.vue index ba53153..70310b3 100644 --- a/src/views/virtualization/VncNodeManage.vue +++ b/src/views/virtualization/VncNodeManage.vue @@ -39,6 +39,18 @@ {{ row.ip || '-' }}:{{ row.port || '-' }} + + + + + +