feat: 工单系统优化 - 修复自动跳转问题并添加用户筛选功能 #18

Merged
shiran merged 94 commits from master into deploy 2026-06-02 17:54:48 +08:00
2 changed files with 73 additions and 5 deletions
Showing only changes of commit 802eaa396b - Show all commits
+64 -3
View File
@@ -129,7 +129,15 @@
</el-button>
</span>
</div>
<div class="config-cell"><span class="config-label">流量上限</span><span class="config-value">{{ formatTraffic(vm.traffic_max) }}</span></div>
<div class="config-cell">
<span class="config-label">流量上限</span>
<span class="config-value">
{{ formatTraffic(vm.traffic_max) }}
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleCommand('updateTraffic')">
<el-icon :size="14"><Edit /></el-icon>修改
</el-button>
</span>
</div>
<div class="config-cell"><span class="config-label">续费价格</span><span class="config-value">¥{{ (userGoods.renewPrice / 100).toFixed(2) }}</span></div>
<div class="config-cell"><span class="config-label">基础价格</span><span class="config-value">¥{{ (userGoods.basePrice / 100).toFixed(2) }}</span></div>
</div>
@@ -306,6 +314,13 @@
<el-table-column prop="gateway" label="网关" min-width="120" />
<el-table-column prop="mac_address" label="MAC" min-width="150" show-overflow-tooltip />
<el-table-column label="类型" width="80"><template #default="{ row }"><el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.type === 'bridge' ? '网桥' : 'NAT' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-button link type="danger" size="small" :loading="deletingNetworkId === row.id" @click="handleDeleteVmNetwork(row)">
<el-icon :size="14"><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!vmNetworks.length" :image-size="60" description="暂无网络" />
</el-tab-pane>
@@ -1072,7 +1087,7 @@
import { ref, reactive, computed, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled, View, Hide, CopyDocument } from '@element-plus/icons-vue'
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled, View, Hide, CopyDocument, Edit, Delete } from '@element-plus/icons-vue'
import {
getUserVmDetail, getUserVmVnc, getUserVmHostImages,
startUserVm, stopUserVm, rebootUserVm, suspendUserVm, resumeUserVm, rescueUserVm, exitRescueUserVm, rebuildUserVm, deleteUserVm,
@@ -1083,11 +1098,12 @@ import {
getUserVmPostGroupList, createUserVmPostGroup, updateUserVmPostGroup, bindUserVmPostGroup, unbindUserVmPostGroup, applyUserVmPostGroup, deleteUserVmPostGroup, enableUserVmPostGroupWhitelist, disableUserVmPostGroupWhitelist,
createUserVmPostGroupRule, updateUserVmPostGroupRule, deleteUserVmPostGroupRule,
getUserVmPostGroupDetail,
getUserVmNetworkList, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
getUserVmNetworkList, getUserVmNetworkDetail, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
getUserGoodsDetail,
getUserVmMetricsHistory,
getUserVmTrafficPolicy, updateUserVmTrafficPolicy, addUserVmFixedTraffic, addUserVmTemporaryTraffic
} from '@/api/admin/userVm'
import { deleteNetwork as deletePointNetwork } from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
import UserSelector from '@/components/UserSelector/index.vue'
@@ -1644,6 +1660,49 @@ const loadNetworks = async () => {
} catch { /* */ } finally { networkLoading.value = false }
}
// ---- 删除网络 ----
// 走 host_service/point/network/delete:删除底层物理网络(破坏性,会影响其他绑定该网络的 VM)
// row 字段可能不完整,service_id / host_id 通过 getUserVmNetworkDetail 兜底反查
const deletingNetworkId = ref(0)
const resolveNetworkServiceHost = async (row) => {
let serviceId = row.service_id ?? row.host_service_id ?? row.kvm_service_id ?? 0
let hostId = row.host_id ?? 0
if (!serviceId || !hostId) {
try {
const res = await getUserVmNetworkDetail({ user_goods_id: userGoodsId.value, id: row.id })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
const n = d.data || d.network || d
if (!serviceId) serviceId = n?.service_id ?? n?.host_service_id ?? n?.kvm_service_id ?? 0
if (!hostId) hostId = n?.host_id ?? 0
}
} catch { /* 兜底失败时返回原值,由调用方判断 */ }
}
return { serviceId, hostId }
}
const handleDeleteVmNetwork = (row) => {
ElMessageBox.confirm(
`将删除底层网络「${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, '删除失败'))
} catch (e) {
ElMessage.error(extractApiError(e?.response?.data, '删除失败'))
} finally {
deletingNetworkId.value = 0
}
}).catch(() => {})
}
// ---- 绑定网络 ----
const showBindNetworkSelector = ref(false)
@@ -2355,6 +2414,8 @@ onBeforeUnmount(() => { disposeCharts() })
.config-cell:last-child { border-right: none; }
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
.config-value { font-size: 13px; color: #1d2129; line-height: 1.4; word-break: break-all; }
.cfg-edit-btn { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
.pwd-value { display: inline-flex; align-items: center; gap: 4px; }
.pwd-text { font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; background: #f5f7fa; padding: 2px 8px; border-radius: 3px; letter-spacing: .5px; user-select: all; }
.pwd-btn { padding: 0 !important; height: auto !important; min-height: auto !important; }
+9 -2
View File
@@ -222,7 +222,12 @@
<div class="config-row">
<div class="config-cell">
<span class="config-label">流量上限</span>
<span class="config-value">{{ formatTrafficMax(detail.traffic_max) }}</span>
<span class="config-value">
{{ formatTrafficMax(detail.traffic_max) }}
<el-button link type="primary" size="small" class="cfg-edit-btn" @click="handleUpdateTraffic" :disabled="isMigrating">
<el-icon :size="14"><Edit /></el-icon>修改
</el-button>
</span>
</div>
<div class="config-cell">
<span class="config-label">快照配额</span>
@@ -1564,7 +1569,7 @@
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Refresh, ArrowDown, Plus, Search, WarningFilled, Loading } from '@element-plus/icons-vue'
import { ArrowLeft, Refresh, ArrowDown, Plus, Search, WarningFilled, Loading, Edit } from '@element-plus/icons-vue'
import {
getVmDetail, getVmStatus,
startVm, stopVm, rebootVm, suspendVm, resumeVm,
@@ -3692,6 +3697,8 @@ onMounted(() => { isPageActive = true; initPage() })
.config-cell:last-child { border-right: none; }
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
.config-value { font-size: 14px; color: #1d2129; line-height: 1.4; word-break: break-all; }
.cfg-edit-btn { margin-left: 8px; padding: 0 4px; height: 18px; vertical-align: middle; }
.cfg-edit-btn .el-icon { margin-right: 2px; vertical-align: -2px; }
.spec-value { font-size: 13px; color: #4e5969; }
.ip-value { color: #165dff; font-weight: 500; }
.password-cell { display: flex; align-items: center; gap: 8px; }