475 lines
14 KiB
Vue
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>
|