初始提交
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user