feat: 将页面添加分页
Build and Deploy Vue3 / build (push) Successful in 1m35s
Build and Deploy Vue3 / deploy (push) Successful in 1m5s

This commit is contained in:
2026-03-21 17:37:06 +08:00
parent 9edb59d16e
commit 25d782b050
18 changed files with 2220 additions and 154 deletions
+162 -37
View File
@@ -156,7 +156,7 @@
<h3 class="section-title">网络信息</h3>
<el-button size="small" type="primary" @click="handleAddNetwork">添加网络</el-button>
</div>
<el-table v-if="vmNetworks.length" :data="vmNetworks" size="small" stripe>
<el-table v-if="vmNetworks.length" :data="pagedNetworks" size="small" stripe>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column prop="address" label="IP地址" min-width="140" show-overflow-tooltip />
@@ -174,6 +174,12 @@
</el-table-column>
</el-table>
<el-empty v-else description="暂无网络" :image-size="60" />
<div class="pagination-wrapper" v-if="vmNetworks.length > 0">
<el-pagination v-model:current-page="networkPage" v-model:page-size="networkPageSize"
:page-sizes="[10, 20, 50]" :total="vmNetworks.length" layout="total, sizes, prev, pager, next" small
@size-change="s => { networkPageSize = s; networkPage = 1 }"
@current-change="p => { networkPage = p }" />
</div>
</div>
</el-tab-pane>
@@ -183,7 +189,7 @@
<h3 class="section-title">磁盘卷信息</h3>
<el-button size="small" type="primary" @click="handleAddVolume">添加数据卷</el-button>
</div>
<el-table v-if="vmVolumes.length" :data="vmVolumes" size="small" stripe>
<el-table v-if="vmVolumes.length" :data="pagedVolumes" size="small" stripe>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column label="大小" width="80">
@@ -213,6 +219,12 @@
</el-table-column>
</el-table>
<el-empty v-else description="暂无磁盘卷" :image-size="60" />
<div class="pagination-wrapper" v-if="vmVolumes.length > 0">
<el-pagination v-model:current-page="volumePage" v-model:page-size="volumePageSize"
:page-sizes="[10, 20, 50]" :total="vmVolumes.length" layout="total, sizes, prev, pager, next" small
@size-change="s => { volumePageSize = s; volumePage = 1 }"
@current-change="p => { volumePage = p }" />
</div>
</div>
</el-tab-pane>
@@ -222,7 +234,7 @@
<h3 class="section-title">安全组管理</h3>
<el-button size="small" type="primary" @click="handleBindSgFromTab">绑定安全组</el-button>
</div>
<el-table v-if="vmSecurityGroups.length" :data="vmSecurityGroups" size="small" stripe>
<el-table v-if="vmSecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="note" label="备注" min-width="160" show-overflow-tooltip />
@@ -248,6 +260,12 @@
</el-table-column>
</el-table>
<el-empty v-else description="暂无绑定的安全组" :image-size="60" />
<div class="pagination-wrapper" v-if="vmSecurityGroups.length > 0">
<el-pagination v-model:current-page="securityPage" v-model:page-size="securityPageSize"
:page-sizes="[10, 20, 50]" :total="vmSecurityGroups.length" layout="total, sizes, prev, pager, next" small
@size-change="s => { securityPageSize = s; securityPage = 1 }"
@current-change="p => { securityPage = p }" />
</div>
</div>
</el-tab-pane>
@@ -255,7 +273,9 @@
<div class="section-block">
<div class="section-header">
<h3 class="section-title">快照管理</h3>
<div style="display: flex; gap: 8px">
<div style="display: flex; align-items: center; gap: 8px">
<el-tag v-if="snapshotQuota" size="small" effect="plain">{{ snapshotQuota.count }} / {{ snapshotQuota.limit }}</el-tag>
<el-button size="small" @click="handleSetSnapshotLimit">设置上限</el-button>
<el-button size="small" type="primary" @click="handleCreateSnapshot">创建快照</el-button>
<el-button size="small" @click="loadSnapshots">刷新</el-button>
</div>
@@ -263,15 +283,22 @@
<el-table :data="snapshotList" v-loading="snapshotLoading" stripe size="small" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="taskStatusType(row.status)" size="small">{{ snapshotStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="任务ID" min-width="160">
<template #default="{ row }">
<span style="font-family: Consolas, monospace; font-size: 12px">{{ row.task_id || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
</el-table-column>
<el-table-column label="更新时间" width="170">
<template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleRestoreSnapshot(row)">恢复</el-button>
@@ -281,6 +308,12 @@
</el-table-column>
</el-table>
<el-empty v-if="!snapshotList.length && !snapshotLoading" description="暂无快照" :image-size="60" />
<div class="pagination-wrapper" v-if="snapshotTotal > 0">
<el-pagination v-model:current-page="snapshotPage" v-model:page-size="snapshotPageSize"
:page-sizes="[10, 20, 50]" :total="snapshotTotal" layout="total, sizes, prev, pager, next" small
@size-change="s => { snapshotPageSize = s; snapshotPage = 1; loadSnapshots() }"
@current-change="p => { snapshotPage = p; loadSnapshots() }" />
</div>
</div>
</el-tab-pane>
@@ -288,7 +321,9 @@
<div class="section-block">
<div class="section-header">
<h3 class="section-title">备份管理</h3>
<div style="display: flex; gap: 8px">
<div style="display: flex; align-items: center; gap: 8px">
<el-tag v-if="backupQuota" size="small" effect="plain">{{ backupQuota.count }} / {{ backupQuota.limit }}</el-tag>
<el-button size="small" @click="handleSetBackupLimit">设置上限</el-button>
<el-button size="small" type="primary" @click="handleCreateBackup">创建备份</el-button>
<el-button size="small" @click="loadBackups">刷新</el-button>
</div>
@@ -296,15 +331,22 @@
<el-table :data="backupList" v-loading="backupLoading" stripe size="small" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="taskStatusType(row.status)" size="small">{{ snapshotStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="任务ID" min-width="160">
<template #default="{ row }">
<span style="font-family: Consolas, monospace; font-size: 12px">{{ row.task_id || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
</el-table-column>
<el-table-column label="更新时间" width="170">
<template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleRestoreBackup(row)">恢复</el-button>
@@ -314,6 +356,12 @@
</el-table-column>
</el-table>
<el-empty v-if="!backupList.length && !backupLoading" description="暂无备份" :image-size="60" />
<div class="pagination-wrapper" v-if="backupTotal > 0">
<el-pagination v-model:current-page="backupPage" v-model:page-size="backupPageSize"
:page-sizes="[10, 20, 50]" :total="backupTotal" layout="total, sizes, prev, pager, next" small
@size-change="s => { backupPageSize = s; backupPage = 1; loadBackups() }"
@current-change="p => { backupPage = p; loadBackups() }" />
</div>
</div>
</el-tab-pane>
@@ -384,9 +432,6 @@
<el-form-item label="快照名称" required>
<el-input v-model="snapshotForm.name" placeholder="请输入快照名称" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="snapshotForm.description" type="textarea" :rows="2" placeholder="可选描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="snapshotCreateVisible = false">取消</el-button>
@@ -401,9 +446,6 @@
<el-form-item label="备份名称" required>
<el-input v-model="backupForm.name" placeholder="请输入备份名称" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="backupForm.description" type="textarea" :rows="2" placeholder="可选描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="backupCreateVisible = false">取消</el-button>
@@ -760,8 +802,8 @@ import {
createNetwork, updateNetwork, deleteNetwork, getNetworkList,
createVolume, resizeVolume, mountVolume, unmountVolume, transferVolume, deleteVolume, getVolumeList,
getVmList,
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress,
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import * as echarts from 'echarts'
@@ -784,6 +826,20 @@ const detail = ref(null)
const vmNetworks = ref([])
const vmVolumes = ref([])
const vmImage = ref(null)
const networkPage = ref(1)
const networkPageSize = ref(10)
const pagedNetworks = computed(() => {
const start = (networkPage.value - 1) * networkPageSize.value
return vmNetworks.value.slice(start, start + networkPageSize.value)
})
const volumePage = ref(1)
const volumePageSize = ref(10)
const pagedVolumes = computed(() => {
const start = (volumePage.value - 1) * volumePageSize.value
return vmVolumes.value.slice(start, start + volumePageSize.value)
})
const vmPortGroup = ref(null)
const metricsData = ref(null)
const hostOptions = ref([])
@@ -1275,6 +1331,12 @@ const submitGetVnc = async () => {
// ---- 安全组管理(标签页列表) ----
const vmSecurityGroups = ref([])
const sgListLoading = ref(false)
const securityPage = ref(1)
const securityPageSize = ref(10)
const pagedSecurityGroups = computed(() => {
const start = (securityPage.value - 1) * securityPageSize.value
return vmSecurityGroups.value.slice(start, start + securityPageSize.value)
})
const loadVmSecurityGroups = async () => {
sgListLoading.value = true
@@ -1595,12 +1657,20 @@ const submitTransferVolume = async () => {
// ---- 快照/备份管理 ----
const snapshotList = ref([])
const snapshotLoading = ref(false)
const snapshotQuota = ref(null)
const snapshotPage = ref(1)
const snapshotPageSize = ref(10)
const snapshotTotal = ref(0)
const backupList = ref([])
const backupLoading = ref(false)
const backupQuota = ref(null)
const backupPage = ref(1)
const backupPageSize = ref(10)
const backupTotal = ref(0)
const snapshotCreateVisible = ref(false)
const backupCreateVisible = ref(false)
const snapshotForm = reactive({ name: '', description: '' })
const backupForm = reactive({ name: '', description: '' })
const snapshotForm = reactive({ name: '' })
const backupForm = reactive({ name: '' })
const taskProgressVisible = ref(false)
const taskProgressLoading = ref(false)
const taskProgressData = ref(null)
@@ -1625,29 +1695,85 @@ const taskProgressMeta = computed(() => {
const loadSnapshots = async () => {
snapshotLoading.value = true
try {
const res = await getSnapshotList({ service_id: serviceId.value })
const res = await getSnapshotList({ service_id: serviceId.value, vm_id: vmId.value, page: snapshotPage.value, page_size: snapshotPageSize.value })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
const all = d.snapshots || d.data || d.list || (Array.isArray(d) ? d : [])
snapshotList.value = all.filter(s => s.vm_id === vmId.value || s.vm_id === String(vmId.value))
} else snapshotList.value = []
} catch { snapshotList.value = [] } finally { snapshotLoading.value = false }
snapshotList.value = d.data || d.list || (Array.isArray(d) ? d : [])
snapshotTotal.value = d.meta?.count ?? d.total ?? snapshotList.value.length
} else { snapshotList.value = []; snapshotTotal.value = 0 }
} catch { snapshotList.value = []; snapshotTotal.value = 0 } finally { snapshotLoading.value = false }
}
const loadSnapshotQuota = async () => {
try {
const res = await getSnapshotCount({ service_id: serviceId.value, vm_id: vmId.value })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
snapshotQuota.value = { count: d.count ?? 0, limit: d.limit ?? 0 }
}
} catch { /* ignore */ }
}
const handleSetSnapshotLimit = () => {
ElMessageBox.prompt('请输入快照数量上限', '设置快照上限', {
confirmButtonText: '确定', cancelButtonText: '取消',
inputPattern: /^[1-9]\d*$/, inputErrorMessage: '请输入正整数',
inputValue: String(snapshotQuota.value?.limit || 10)
}).then(async ({ value }) => {
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', vmId.value)
fd.append('limit', value)
const res = await setSnapshotLimit(fd)
if (res?.data?.code === 200) { ElMessage.success('快照上限设置成功'); loadSnapshotQuota() }
else ElMessage.error(extractApiError(res?.data, '设置失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置失败')) }
}).catch(() => {})
}
const loadBackups = async () => {
backupLoading.value = true
try {
const res = await getBackupList({ service_id: serviceId.value })
const res = await getBackupList({ service_id: serviceId.value, vm_id: vmId.value, page: backupPage.value, page_size: backupPageSize.value })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
const all = d.backups || d.data || d.list || (Array.isArray(d) ? d : [])
backupList.value = all.filter(b => b.vm_id === vmId.value || b.vm_id === String(vmId.value))
} else backupList.value = []
} catch { backupList.value = [] } finally { backupLoading.value = false }
backupList.value = d.data || d.list || (Array.isArray(d) ? d : [])
backupTotal.value = d.meta?.count ?? d.total ?? backupList.value.length
} else { backupList.value = []; backupTotal.value = 0 }
} catch { backupList.value = []; backupTotal.value = 0 } finally { backupLoading.value = false }
}
const loadBackupQuota = async () => {
try {
const res = await getBackupCount({ service_id: serviceId.value, vm_id: vmId.value })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
backupQuota.value = { count: d.count ?? 0, limit: d.limit ?? 0 }
}
} catch { /* ignore */ }
}
const handleSetBackupLimit = () => {
ElMessageBox.prompt('请输入备份数量上限', '设置备份上限', {
confirmButtonText: '确定', cancelButtonText: '取消',
inputPattern: /^[1-9]\d*$/, inputErrorMessage: '请输入正整数',
inputValue: String(backupQuota.value?.limit || 10)
}).then(async ({ value }) => {
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', vmId.value)
fd.append('limit', value)
const res = await setBackupLimit(fd)
if (res?.data?.code === 200) { ElMessage.success('备份上限设置成功'); loadBackupQuota() }
else ElMessage.error(extractApiError(res?.data, '设置失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置失败')) }
}).catch(() => {})
}
const handleCreateSnapshot = () => {
Object.assign(snapshotForm, { name: '', description: '' })
Object.assign(snapshotForm, { name: '' })
snapshotCreateVisible.value = true
}
const submitCreateSnapshot = async () => {
@@ -1658,9 +1784,8 @@ const submitCreateSnapshot = async () => {
fd.append('service_id', serviceId.value)
fd.append('vm_id', vmId.value)
fd.append('name', snapshotForm.name)
if (snapshotForm.description) fd.append('description', snapshotForm.description)
const res = await createSnapshot(fd)
if (res?.data?.code === 200) { ElMessage.success('快照创建成功'); snapshotCreateVisible.value = false; loadSnapshots() }
if (res?.data?.code === 200) { ElMessage.success('快照创建成功'); snapshotCreateVisible.value = false; loadSnapshots(); loadSnapshotQuota() }
else ElMessage.error(extractApiError(res?.data, '创建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
}
@@ -1689,7 +1814,7 @@ const handleDeleteSnapshot = (row) => {
fd.append('snapshot_id', row.id)
fd.append('vm_id', row.vm_id)
const res = await deleteSnapshot(fd)
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadSnapshots() }
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadSnapshots(); loadSnapshotQuota() }
else ElMessage.error(extractApiError(res?.data, '删除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
}).catch(() => {})
@@ -1708,7 +1833,7 @@ const handleSnapshotProgress = async (row) => {
}
const handleCreateBackup = () => {
Object.assign(backupForm, { name: '', description: '' })
Object.assign(backupForm, { name: '' })
backupCreateVisible.value = true
}
const submitCreateBackup = async () => {
@@ -1719,9 +1844,8 @@ const submitCreateBackup = async () => {
fd.append('service_id', serviceId.value)
fd.append('vm_id', vmId.value)
fd.append('name', backupForm.name)
if (backupForm.description) fd.append('description', backupForm.description)
const res = await createBackup(fd)
if (res?.data?.code === 200) { ElMessage.success('备份创建成功'); backupCreateVisible.value = false; loadBackups() }
if (res?.data?.code === 200) { ElMessage.success('备份创建成功'); backupCreateVisible.value = false; loadBackups(); loadBackupQuota() }
else ElMessage.error(extractApiError(res?.data, '创建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
}
@@ -1750,7 +1874,7 @@ const handleDeleteBackup = (row) => {
fd.append('backup_id', row.id)
fd.append('vm_id', row.vm_id)
const res = await deleteBackup(fd)
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadBackups() }
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadBackups(); loadBackupQuota() }
else ElMessage.error(extractApiError(res?.data, '删除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
}).catch(() => {})
@@ -1793,8 +1917,8 @@ watch(activeTab, (tab) => {
if (tab === 'network') loadVmNetworks()
if (tab === 'volume') loadVmVolumes()
if (tab === 'security') loadVmSecurityGroups()
if (tab === 'snapshot') loadSnapshots()
if (tab === 'backup') loadBackups()
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
})
onActivated(() => {
isPageActive = true
@@ -1872,4 +1996,5 @@ onMounted(() => { isPageActive = true; initPage() })
.net-speed-value { font-size: 20px; font-weight: 600; color: #1d2129; }
.vnc-result { margin-top: 12px; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
</style>