- 重新加载 - -
-
-
- +
+ @@ -142,14 +285,15 @@ diff --git a/src/views/product/UserGoodsList.vue b/src/views/product/UserGoodsList.vue index 02fad65..016bb3e 100644 --- a/src/views/product/UserGoodsList.vue +++ b/src/views/product/UserGoodsList.vue @@ -1,6 +1,38 @@ + + + @@ -464,8 +513,8 @@ import { ref, reactive, computed, onMounted, watch } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Refresh, Search, ArrowDown } from '@element-plus/icons-vue' -import { getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods, getUserVmList, getExpireRemindList, sendExpireRemind } from '@/api/admin/userVm' +import { Plus, Refresh, Search, ArrowDown, Box, CircleCheck, Clock, Delete } 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' import { getProductParameterList, getProductPlanDetail } from '@/api/admin/product' @@ -480,12 +529,16 @@ const router = useRouter() const loading = ref(false) const list = ref([]) const total = ref(0) -const query = reactive({ page: 1, count: 10, key: '', user_id: '', good_id: '' }) +const query = reactive({ page: 1, count: 10, key: '', user_id: '', good_id: '', status: 'all' }) const filterUserName = ref('') const filterGoodName = ref('') 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 formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-' const formatExpireTime = (t) => { @@ -495,6 +548,17 @@ const formatExpireTime = (t) => { return d.format('YYYY-MM-DD HH:mm:ss') } +// 判断用户商品是否已删除(后端返回 deleteAt 字段,有值即已删除) +const isDeleted = (row) => !!(row?.deleteAt || row?.DeleteAt || row?.deleted_at) + +// 判断用户商品是否已到期(永久商品除外) +const isExpired = (row) => { + const t = row?.expireTime || row?.expire_time + if (!t) return false + const d = dayjs(t) + return d.year() >= 2000 && d.isBefore(dayjs()) +} + const formatMemory = (kb) => { if (!kb) return '-' if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB' @@ -520,13 +584,20 @@ const getStatusText = (status) => { } } +// 组装筛选参数(count 与 list 共用) +const buildFilterParams = () => { + const params = {} + if (query.key) params.key = query.key + if (query.user_id) params.user_id = parseInt(query.user_id) || undefined + if (query.good_id) params.good_id = parseInt(query.good_id) || undefined + return params +} + const loadList = async () => { loading.value = true try { - const params = { page: query.page, count: query.count } - if (query.key) params.key = query.key - if (query.user_id) params.user_id = parseInt(query.user_id) || undefined - if (query.good_id) params.good_id = parseInt(query.good_id) || undefined + const params = { page: query.page, count: query.count, ...buildFilterParams() } + if (query.status && query.status !== 'all') params.status = query.status const res = await getUserGoodsList(params) if (res?.data?.code === 200 && res?.data?.data) { const d = res.data.data @@ -536,7 +607,26 @@ const loadList = async () => { } catch { list.value = []; total.value = 0 } finally { loading.value = false } } -const handleSearch = () => { query.page = 1; loadList() } +// 加载数量统计(透传筛选条件,与列表联动) +const loadCount = async () => { + try { + const res = await getUserGoodsCount(buildFilterParams()) + if (res?.data?.code === 200 && res?.data?.data) { + const d = res.data.data + counts.normal = d.normal ?? 0 + counts.deleted = d.deleted ?? 0 + counts.expired = d.expired ?? 0 + } + } catch { /* 统计失败不阻断列表 */ } +} + +const handleSearch = () => { query.page = 1; loadList(); loadCount() } + +// 点击统计卡片快速切换状态筛选 +const handleStatusCard = (status) => { + query.status = status + handleSearch() +} // ---- 订单参数配置 ---- const argsSpecList = ref([]) @@ -841,12 +931,7 @@ const submitEdit = async () => { // ---- 详情 / 删除 ---- const handleDetail = (row) => { - const tag = (row.tag || row.good?.tag || '').toLowerCase() - if (tag === '云服务器') { - router.push({ path: '/user-goods/vm-detail', query: { id: row.id } }) - } else { - router.push({ name: 'UserGoodsDetail', params: { id: row.id } }) - } + router.push({ name: 'UserGoodsDetail', params: { id: row.id } }) } const handleMoreCmd = (cmd, row) => { @@ -911,7 +996,7 @@ const handleSendRemind = (row) => { }).catch(() => {}) } -onMounted(loadList) +onMounted(() => { loadList(); loadCount() }) diff --git a/src/views/virtualization/VmManage.vue b/src/views/virtualization/VmManage.vue index 693fd2f..bad8ec8 100644 --- a/src/views/virtualization/VmManage.vue +++ b/src/views/virtualization/VmManage.vue @@ -28,8 +28,24 @@
+ +
+ 已选择 {{ selectedVms.length }} 台虚拟机 + + 批量开机 + + + 批量关机 + + + 批量删除 + + 取消选择 +
+ - + + @@ -433,7 +449,7 @@ import { ref, reactive, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled } from '@element-plus/icons-vue' +import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled, VideoPlay, SwitchButton, Delete } from '@element-plus/icons-vue' import { getRemoteHostList, getVmList, getVmDetail, getVmStatus, createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm, @@ -466,6 +482,7 @@ const serviceName = computed(() => injectedServiceName?.value || route.query.ser const loading = ref(false) const submitLoading = ref(false) const detailLoading = ref(false) +const batchLoading = ref(false) const vmList = ref([]) const total = ref(0) const keyword = ref('') @@ -473,6 +490,12 @@ const filterStatus = ref('') const hostOptions = ref([]) const queryParams = reactive({ page: 1, page_size: 10 }) +// 批量操作 +const vmTableRef = ref(null) +const selectedVms = ref([]) +const handleSelectionChange = (selection) => { selectedVms.value = selection } +const clearSelection = () => { vmTableRef.value?.clearSelection() } + // 选择器 const showCreateImageSelector = ref(false) const showRebuildImageSelector = ref(false) @@ -989,6 +1012,72 @@ const handleDelete = (row) => { }).catch(() => {}) } +// 批量电源操作(开机/关机) +const handleBatchPower = (action) => { + const label = action === 'start' ? '开机' : '关机' + const targets = selectedVms.value + const skipped = targets.filter(v => + action === 'start' ? v.status === 'running' : (v.status === 'stopped' || v.status === 'stop') + ) + const toOperate = targets.filter(v => !skipped.includes(v)) + + if (!toOperate.length) { + ElMessage.warning(`所选虚拟机均已处于目标状态,无需${label}`) + return + } + + const msg = skipped.length + ? `将对 ${toOperate.length} 台虚拟机执行${label}(${skipped.length} 台已跳过),是否继续?` + : `确定要对 ${toOperate.length} 台虚拟机执行批量${label}吗?` + + ElMessageBox.confirm(msg, `批量${label}`, { + confirmButtonText: '确定', cancelButtonText: '取消', + type: action === 'stop' ? 'warning' : 'info' + }).then(async () => { + batchLoading.value = true + const api = action === 'start' ? startVm : stopVm + let success = 0, fail = 0 + await Promise.allSettled(toOperate.map(async (vm) => { + try { + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('vm_id', vm.id) + const res = await api(fd) + if (res?.data?.code === 200) success++ + else fail++ + } catch { fail++ } + })) + batchLoading.value = false + clearSelection() + ElMessage[fail ? 'warning' : 'success'](`批量${label}完成:成功 ${success},失败 ${fail}`) + loadList() + }).catch(() => {}) +} + +// 批量删除 +const handleBatchDelete = () => { + const count = selectedVms.value.length + ElMessageBox.confirm( + `确定要删除选中的 ${count} 台虚拟机吗?此操作不可恢复。`, + '批量删除', + { confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' } + ).then(async () => { + batchLoading.value = true + let success = 0, fail = 0 + await Promise.allSettled(selectedVms.value.map(async (vm) => { + try { + const res = await deleteVm({ service_id: serviceId.value, vm_id: vm.id }) + if (res?.data?.code === 200) success++ + else fail++ + } catch { fail++ } + })) + batchLoading.value = false + clearSelection() + ElMessage[fail ? 'warning' : 'success'](`批量删除完成:成功 ${success},失败 ${fail}`) + loadList() + }).catch(() => {}) +} + const goBack = () => { router.push('/virtualization/kvm-service') } onMounted(async () => { @@ -1007,4 +1096,19 @@ defineExpose({ loadList }) .migrate-inline-status { display: flex; align-items: center; gap: 6px; margin-top: 4px; } .migrate-inline-label { color: #e6a23c; font-size: 13px; font-weight: 600; white-space: nowrap; } .migrate-inline-pct { color: #e6a23c; font-size: 12px; white-space: nowrap; } +.batch-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + margin-bottom: 12px; + background: #ecf5ff; + border: 1px solid #d9ecff; + border-radius: 6px; +} +.batch-info { + font-size: 13px; + color: #409eff; + margin-right: 4px; +} -- 2.52.0