Files
kvm/src/views/image/ImageList.vue
T
2026-02-12 15:40:10 +08:00

475 lines
14 KiB
Vue

<template>
<div class="image-list-container">
<a-card class="glass-card">
<template #title>
<span>镜像列表</span>
</template>
<template #extra>
<a-button type="primary" @click="handleCreate">
<plus-outlined />
创建镜像
</a-button>
</template>
<div class="search-bar">
<a-input-search
v-model:value="searchKey"
placeholder="搜索镜像名称"
style="width: 300px"
@search="handleSearch"
allow-clear
/>
<a-select
v-model:value="filterOsType"
placeholder="操作系统类型"
style="width: 150px"
allow-clear
@change="handleSearch"
>
<a-select-option value="linux">Linux</a-select-option>
<a-select-option value="windows">Windows</a-select-option>
</a-select>
<a-button @click="handleRefresh">
<reload-outlined />
刷新
</a-button>
</div>
<a-table
:columns="columns"
:data-source="imageList"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'os_type'">
<a-tag :color="record.os_type === 'linux' ? 'blue' : 'purple'">
{{ record.os_type }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ record.status }}
</a-tag>
</template>
<template v-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleReload(record)">
重新下载
</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
<!-- 查看镜像详情 -->
<a-modal
v-model:open="detailVisible"
title="镜像详情"
:footer="null"
width="560px"
>
<a-spin :spinning="detailLoading">
<a-descriptions
v-if="detailData"
:column="1"
bordered
size="small"
>
<a-descriptions-item label="ID">{{ detailData.id }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ detailData.name }}</a-descriptions-item>
<a-descriptions-item label="大小">{{ detailData.size != null ? detailData.size : '—' }}</a-descriptions-item>
<a-descriptions-item label="操作系统类型">
<a-tag :color="detailData.os_type === 'linux' ? 'blue' : 'purple'">
{{ detailData.os_type }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="类型">{{ detailData.type }}</a-descriptions-item>
<a-descriptions-item label="来源">{{ detailData.source ?? '—' }}</a-descriptions-item>
<a-descriptions-item label="源路径">{{ detailData.source_path ?? '—' }}</a-descriptions-item>
<a-descriptions-item label="存储路径">{{ detailData.path ?? '—' }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(detailData.status)">
{{ detailData.status }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailData.created_at ?? '—' }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ detailData.updated_at ?? '—' }}</a-descriptions-item>
<a-descriptions-item v-if="detailData.deleted_at" label="删除时间">
{{ detailData.deleted_at }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-modal>
<!-- 创建镜像 -->
<a-modal
v-model:open="createVisible"
title="创建镜像"
ok-text="创建"
:confirm-loading="createSubmitting"
@ok="submitCreate"
@cancel="resetCreateForm"
>
<a-form
ref="createFormRef"
:model="createForm"
:rules="createRules"
layout="vertical"
>
<a-form-item label="镜像名称" name="name" required>
<a-input v-model:value="createForm.name" placeholder="请输入镜像名称" />
</a-form-item>
<a-form-item label="下载地址" name="url" required>
<a-input v-model:value="createForm.url" placeholder="镜像文件 URL(如 http(s) 或 file 路径)" />
</a-form-item>
<a-form-item label="操作系统类型" name="os_type">
<a-select v-model:value="createForm.os_type" placeholder="选择操作系统类型">
<a-select-option value="linux">Linux</a-select-option>
<a-select-option value="windows">Windows</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="镜像类型" name="type">
<a-select v-model:value="createForm.type" placeholder="选择镜像类型">
<a-select-option value="system">system系统</a-select-option>
<a-select-option value="data">data数据</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- 编辑镜像 -->
<a-modal
v-model:open="editVisible"
title="编辑镜像"
ok-text="保存"
:confirm-loading="editSubmitting"
@ok="submitEdit"
@cancel="resetEditForm"
>
<a-spin :spinning="editLoading">
<a-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
layout="vertical"
>
<a-form-item label="ID">
<a-input v-model:value="editForm.id" disabled />
</a-form-item>
<a-form-item label="镜像名称" name="name" required>
<a-input v-model:value="editForm.name" placeholder="请输入镜像名称" />
</a-form-item>
<a-form-item label="路径" name="path" required>
<a-input v-model:value="editForm.path" placeholder="镜像存储路径" />
</a-form-item>
<a-form-item label="操作系统类型" name="os_type">
<a-select v-model:value="editForm.os_type" placeholder="选择操作系统类型">
<a-select-option value="linux">Linux</a-select-option>
<a-select-option value="windows">Windows</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="类型" name="type">
<a-select v-model:value="editForm.type" placeholder="选择类型">
<a-select-option value="system">system系统</a-select-option>
<a-select-option value="data">data数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="editForm.status" placeholder="选择状态">
<a-select-option value="ready">ready</a-select-option>
<a-select-option value="downloading">downloading</a-select-option>
<a-select-option value="error">error</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</a-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import * as imageApi from '@/api/imageApi'
import { Modal, message } from 'ant-design-vue'
const searchKey = ref('')
const filterOsType = ref(undefined)
const imageList = ref([])
const loading = ref(false)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
})
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '操作系统', key: 'os_type', width: 120 },
{ title: '类型', dataIndex: 'type', key: 'type', width: 120 },
{ title: '状态', key: 'status', width: 100 },
{ title: '路径', dataIndex: 'path', key: 'path' },
{ title: '操作', key: 'actions', width: 300, fixed: 'right' }
]
const getStatusColor = (status) => {
const colorMap = {
ready: 'green',
downloading: 'blue',
error: 'red'
}
return colorMap[status] || 'default'
}
const fetchList = async () => {
loading.value = true
try {
const params = {
page: pagination.value.current,
count: pagination.value.pageSize,
key: searchKey.value || undefined,
os_type: filterOsType.value
}
const data = await imageApi.getImageList(params)
// 处理API返回的数据格式
const responseData = data.data || data
imageList.value = responseData.data || []
pagination.value.total = responseData.count || 0
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.value.current = 1
fetchList()
}
const handleRefresh = () => {
fetchList()
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchList()
}
// 创建镜像
const createVisible = ref(false)
const createFormRef = ref(null)
const createSubmitting = ref(false)
const createForm = reactive({
name: '',
url: '',
os_type: 'linux',
type: 'qcow2'
})
const createRules = {
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
url: [{ required: true, message: '请输入下载地址', trigger: 'blur' }]
}
const resetCreateForm = () => {
createForm.name = ''
createForm.url = ''
createForm.os_type = 'linux'
createForm.type = 'qcow2'
createFormRef.value?.resetFields()
}
const handleCreate = () => {
resetCreateForm()
createVisible.value = true
}
const submitCreate = async () => {
try {
await createFormRef.value?.validate()
} catch (e) {
return
}
createSubmitting.value = true
try {
await imageApi.createImage({
name: createForm.name.trim(),
path: createForm.url.trim(),
os_type: createForm.os_type,
type: createForm.type
})
message.success('创建成功')
createVisible.value = false
fetchList()
} catch (error) {
console.error('创建镜像失败:', error)
} finally {
createSubmitting.value = false
}
}
// 查看镜像详情
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref(null)
const handleView = async (record) => {
detailVisible.value = true
detailData.value = null
detailLoading.value = true
try {
const res = await imageApi.getImageDetail({ image_id: record.id })
// 后端可能返回双层 data(源头 API 被原样包在 data 里),取内层为实际镜像对象
detailData.value = res?.data != null ? res.data : res
} catch (error) {
console.error('获取镜像详情失败:', error)
detailVisible.value = false
} finally {
detailLoading.value = false
}
}
// 编辑镜像
const editVisible = ref(false)
const editFormRef = ref(null)
const editSubmitting = ref(false)
const editLoading = ref(false)
const editForm = reactive({
id: undefined,
name: '',
path: '',
os_type: '',
type: '',
status: ''
})
const editRules = {
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
path: [{ required: true, message: '请输入路径', trigger: 'blur' }]
}
const resetEditForm = () => {
editForm.id = undefined
editForm.name = ''
editForm.path = ''
editForm.os_type = ''
editForm.type = ''
editForm.status = ''
editFormRef.value?.resetFields()
}
const handleEdit = async (record) => {
editVisible.value = true
resetEditForm()
editLoading.value = true
try {
const res = await imageApi.getImageDetail({ image_id: record.id })
const detail = res?.data != null ? res.data : res
if (detail) {
editForm.id = detail.id
editForm.name = detail.name ?? ''
editForm.path = detail.path ?? ''
editForm.os_type = detail.os_type ?? ''
editForm.type = detail.type ?? ''
editForm.status = detail.status ?? ''
}
} catch (error) {
console.error('获取镜像详情失败:', error)
editVisible.value = false
} finally {
editLoading.value = false
}
}
const submitEdit = async () => {
try {
await editFormRef.value?.validate()
} catch (e) {
return
}
editSubmitting.value = true
try {
await imageApi.updateImage({
id: editForm.id,
name: editForm.name.trim(),
path: editForm.path.trim(),
os_type: editForm.os_type,
type: editForm.type,
status: editForm.status
})
message.success('更新成功')
editVisible.value = false
fetchList()
} catch (error) {
console.error('更新镜像失败:', error)
} finally {
editSubmitting.value = false
}
}
const handleReload = async (record) => {
Modal.confirm({
title: '确认重新下载',
content: `确定要重新下载镜像 "${record.name}" 吗?`,
onOk: async () => {
try {
await imageApi.reloadImage({ image_id: record.id })
message.success('开始重新下载')
fetchList()
} catch (error) {
console.error('重新下载失败:', error)
}
}
})
}
const handleDelete = async (record) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.name}" 吗?此操作不可恢复!`,
okType: 'danger',
onOk: async () => {
try {
await imageApi.deleteImage({ image_id: record.id })
message.success('删除成功')
fetchList()
} catch (error) {
console.error('删除失败:', error)
}
}
})
}
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.image-list-container {
padding: 0;
}
.search-bar {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
</style>