Files
ApiServer-Web-admin_dashboa…/src/views/acs/images/ImageRequests.vue
T
wlkjyy f7c3be1d30 refactor: extract image form to standalone page and implement tags view store
- Created ImageForm.vue as standalone page for add/edit image functionality
- Removed dialog-based image form from VmImages.vue
- Implemented tagsViewStore for global tab state management
- Added automatic tab closing on form cancel/back
- Fixed data persistence issue when switching between image edits
- Removed quick actions section from ImageForm
- Updated router configuration for new image form route
2025-11-28 14:15:29 +08:00

793 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="image-requests-container">
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="镜像类型">
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable style="width: 150px">
<el-option label="Docker镜像" value="docker" />
<el-option label="Windows镜像" value="windows" />
</el-select>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
<el-option label="已通过" value="approved" />
<el-option label="审核中" value="pending" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>申请镜像
</el-button>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 提示信息 -->
<el-alert
type="info"
show-icon
:closable="false"
class="info-alert"
style="margin: 20px 20px 0; width: auto;"
>
<template #title>
申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
</template>
</el-alert>
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="申请ID" width="150" align="center" />
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看详情
</el-button>
<el-button
v-if="scope.row.status === 'rejected'"
type="primary"
link
@click="handleResubmit(scope.row)"
>
<el-icon><refresh /></el-icon>重新提交
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 申请镜像对话框 -->
<el-dialog
v-model="requestDialogVisible"
title="申请镜像"
width="700px"
:before-close="handleDialogClose"
>
<el-form :model="requestForm" label-width="120px" :rules="rules" ref="requestFormRef">
<el-form-item label="镜像类型" prop="type">
<el-radio-group v-model="requestForm.type">
<el-radio label="docker">Docker镜像</el-radio>
<el-radio label="windows">Windows镜像</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="镜像名称" prop="name">
<el-input v-model="requestForm.name" placeholder="请输入镜像名称,例如:MySQL 8.0" />
</el-form-item>
<template v-if="requestForm.type === 'docker'">
<el-form-item label="Docker镜像地址" prop="dockerImage">
<el-input v-model="requestForm.dockerImage" placeholder="请输入Docker镜像地址,例如:mysql:8.0">
<template #prepend>
<el-select v-model="requestForm.dockerSource" style="width: 120px">
<el-option label="Docker Hub" value="dockerhub" />
<el-option label="自定义" value="custom" />
</el-select>
</template>
</el-input>
<div class="form-tip">Docker Hub格式mysql:8.0自建仓库格式namespace/repo:tag</div>
</el-form-item>
<el-divider content-position="left">环境变量配置</el-divider>
<div class="env-vars-container">
<div class="env-vars-header">
<div class="env-var-name">变量名</div>
<div class="env-var-value">变量值</div>
<div class="env-var-action"></div>
</div>
<div v-for="(env, index) in requestForm.envVars" :key="index" class="env-vars-item">
<el-input v-model="env.key" placeholder="KEY" />
<el-input v-model="env.value" placeholder="VALUE" />
<el-button circle type="danger" @click="removeEnvVar(index)">
<el-icon><delete /></el-icon>
</el-button>
</div>
<el-button type="primary" plain @click="addEnvVar" class="add-env-btn">
<el-icon><plus /></el-icon>添加环境变量
</el-button>
</div>
<el-divider content-position="left">暴露端口</el-divider>
<div class="ports-container">
<div class="ports-header">
<div class="port-container">容器端口</div>
<div class="port-protocol">协议</div>
<div class="port-desc">描述</div>
<div class="port-action"></div>
</div>
<div v-for="(port, index) in requestForm.ports" :key="index" class="ports-item">
<el-input-number v-model="port.containerPort" :min="1" :max="65535" controls-position="right" />
<el-select v-model="port.protocol">
<el-option label="TCP" value="TCP" />
<el-option label="UDP" value="UDP" />
</el-select>
<el-input v-model="port.description" placeholder="请填写用途描述" />
<el-button circle type="danger" @click="removePort(index)">
<el-icon><delete /></el-icon>
</el-button>
</div>
<el-button type="primary" plain @click="addPort" class="add-port-btn">
<el-icon><plus /></el-icon>添加端口
</el-button>
</div>
</template>
<template v-else>
<el-form-item label="Windows版本" prop="windowsVersion">
<el-select v-model="requestForm.windowsVersion" placeholder="请选择Windows版本" style="width: 100%">
<el-option label="Windows Server 2019" value="2019" />
<el-option label="Windows Server 2022" value="2022" />
<el-option label="Windows 10" value="10" />
<el-option label="Windows 11" value="11" />
</el-select>
</el-form-item>
<el-form-item label="镜像链接" prop="windowsImageUrl">
<el-input v-model="requestForm.windowsImageUrl" placeholder="请输入Windows镜像的下载链接" />
<div class="form-tip">提供ISO镜像的下载链接支持微软官方MSDN等其他合法渠道的镜像</div>
</el-form-item>
<el-form-item label="激活方式" prop="activationMethod">
<el-radio-group v-model="requestForm.activationMethod">
<el-radio label="kms">KMS激活</el-radio>
<el-radio label="key">产品密钥</el-radio>
<el-radio label="none">不需要激活</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-form-item label="申请理由" prop="reason">
<el-input
v-model="requestForm.reason"
type="textarea"
:rows="4"
placeholder="请详细说明申请该镜像的用途和理由"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleDialogClose">取消</el-button>
<el-button type="primary" @click="submitForm">提交申请</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="申请详情"
width="700px"
:before-close="handleDialogClose"
>
<div v-if="currentRequest.id" class="request-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="申请ID" :span="2">{{ currentRequest.id }}</el-descriptions-item>
<el-descriptions-item label="镜像名称">{{ currentRequest.name }}</el-descriptions-item>
<el-descriptions-item label="镜像类型">
<el-tag :type="currentRequest.type === 'docker' ? 'success' : 'primary'">
{{ currentRequest.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ currentRequest.requestTime }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(currentRequest.status)">
{{ getStatusText(currentRequest.status) }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">详细信息</el-divider>
<template v-if="currentRequest.type === 'docker'">
<el-descriptions :column="1" border>
<el-descriptions-item label="Docker镜像地址">
{{ currentRequest.dockerSource === 'dockerhub' ? 'Docker Hub: ' : '自定义: ' }}{{ currentRequest.dockerImage }}
</el-descriptions-item>
</el-descriptions>
<div v-if="currentRequest.envVars && currentRequest.envVars.length > 0">
<el-divider content-position="left">环境变量</el-divider>
<el-table :data="currentRequest.envVars" border style="width: 100%">
<el-table-column prop="key" label="变量名" />
<el-table-column prop="value" label="变量值" />
</el-table>
</div>
<div v-if="currentRequest.ports && currentRequest.ports.length > 0">
<el-divider content-position="left">端口配置</el-divider>
<el-table :data="currentRequest.ports" border style="width: 100%">
<el-table-column prop="containerPort" label="容器端口" width="120" />
<el-table-column prop="protocol" label="协议" width="100" />
<el-table-column prop="description" label="描述" />
</el-table>
</div>
</template>
<template v-else>
<el-descriptions :column="1" border>
<el-descriptions-item label="Windows版本">
{{ getWindowsVersionText(currentRequest.windowsVersion) }}
</el-descriptions-item>
<el-descriptions-item label="镜像链接">
{{ currentRequest.windowsImageUrl }}
</el-descriptions-item>
<el-descriptions-item label="激活方式">
{{ getActivationMethodText(currentRequest.activationMethod) }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-divider content-position="left">申请理由</el-divider>
<div class="request-reason">{{ currentRequest.reason }}</div>
<template v-if="currentRequest.reviewComment">
<el-divider content-position="left">审核意见</el-divider>
<div class="review-comment">{{ currentRequest.reviewComment }}</div>
</template>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import {
Plus, Refresh, Search, View, Delete, InfoFilled
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 搜索表单
const searchForm = reactive({
name: '',
type: '',
status: ''
})
// 重置搜索
const resetSearch = () => {
searchForm.name = ''
searchForm.type = ''
searchForm.status = ''
handleSearch()
}
// 表格数据
const loading = ref(false)
const tableData = ref([])
// 分页
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 处理页码变化
const handleCurrentChange = (val) => {
pagination.currentPage = val
fetchData()
}
// 处理每页条数变化
const handleSizeChange = (val) => {
pagination.pageSize = val
fetchData()
}
// 对话框相关
const requestDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const currentRequest = ref({})
// 表单对象和规则
const requestFormRef = ref(null)
const requestForm = reactive({
type: 'docker',
name: '',
dockerSource: 'dockerhub',
dockerImage: '',
windowsVersion: '',
windowsImageUrl: '',
activationMethod: 'kms',
reason: '',
envVars: [],
ports: []
})
const rules = {
type: [{ required: true, message: '请选择镜像类型', trigger: 'change' }],
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
dockerImage: [{ required: true, message: '请输入Docker镜像地址', trigger: 'blur' }],
windowsVersion: [{ required: true, message: '请选择Windows版本', trigger: 'change' }],
windowsImageUrl: [{ required: true, message: '请输入镜像链接', trigger: 'blur' }],
activationMethod: [{ required: true, message: '请选择激活方式', trigger: 'change' }],
reason: [{ required: true, message: '请输入申请理由', trigger: 'blur' }]
}
// 状态标签样式
const getStatusType = (status) => {
const map = {
approved: 'success',
pending: 'warning',
rejected: 'danger'
}
return map[status] || 'info'
}
const getStatusText = (status) => {
const map = {
approved: '已通过',
pending: '审核中',
rejected: '已拒绝'
}
return map[status] || '未知'
}
// Windows版本文本
const getWindowsVersionText = (version) => {
const map = {
'2019': 'Windows Server 2019',
'2022': 'Windows Server 2022',
'10': 'Windows 10',
'11': 'Windows 11'
}
return map[version] || '未知'
}
// 激活方式文本
const getActivationMethodText = (method) => {
const map = {
kms: 'KMS激活',
key: '产品密钥',
none: '不需要激活'
}
return map[method] || '未知'
}
// 处理搜索
const handleSearch = () => {
pagination.currentPage = 1
fetchData()
}
// 刷新数据
const handleRefresh = () => {
fetchData()
}
// 添加环境变量
const addEnvVar = () => {
requestForm.envVars.push({ key: '', value: '' })
}
// 移除环境变量
const removeEnvVar = (index) => {
requestForm.envVars.splice(index, 1)
}
// 添加端口
const addPort = () => {
requestForm.ports.push({ containerPort: 80, protocol: 'TCP', description: '' })
}
// 移除端口
const removePort = (index) => {
requestForm.ports.splice(index, 1)
}
// 获取数据
const fetchData = () => {
loading.value = true
// 模拟API请求
setTimeout(() => {
// 这里应该是真实的API请求
const mockData = [
{
id: 'REQ20240501001',
name: 'MySQL 8.0',
type: 'docker',
requestTime: '2024-05-01 10:23:45',
status: 'approved',
dockerSource: 'dockerhub',
dockerImage: 'mysql:8.0',
envVars: [
{ key: 'MYSQL_ROOT_PASSWORD', value: 'password' },
{ key: 'MYSQL_DATABASE', value: 'testdb' }
],
ports: [
{ containerPort: 3306, protocol: 'TCP', description: 'MySQL默认端口' }
],
reason: '用于开发测试环境,需要MySQL数据库服务',
reviewComment: '审核通过,已添加到镜像列表'
},
{
id: 'REQ20240502001',
name: 'Windows Server 2022',
type: 'windows',
requestTime: '2024-05-02 14:30:12',
status: 'pending',
windowsVersion: '2022',
windowsImageUrl: 'https://example.com/windows-server-2022.iso',
activationMethod: 'kms',
reason: '用于测试Windows Server 2022的新功能和兼容性'
},
{
id: 'REQ20240503001',
name: 'Redis 7.0',
type: 'docker',
requestTime: '2024-05-03 09:15:36',
status: 'rejected',
dockerSource: 'dockerhub',
dockerImage: 'redis:7.0',
envVars: [],
ports: [
{ containerPort: 6379, protocol: 'TCP', description: 'Redis默认端口' }
],
reason: '用于缓存服务',
reviewComment: '镜像存在安全漏洞,请使用Redis 7.0.2或更高版本'
}
]
tableData.value = mockData
pagination.total = mockData.length
loading.value = false
}, 500)
}
// 申请镜像
const handleAdd = () => {
requestForm.type = 'docker'
requestForm.name = ''
requestForm.dockerSource = 'dockerhub'
requestForm.dockerImage = ''
requestForm.windowsVersion = ''
requestForm.windowsImageUrl = ''
requestForm.activationMethod = 'kms'
requestForm.reason = ''
requestForm.envVars = []
requestForm.ports = []
requestDialogVisible.value = true
}
// 查看详情
const handleView = (row) => {
currentRequest.value = { ...row }
detailDialogVisible.value = true
}
// 重新提交
const handleResubmit = (row) => {
// 复制原有申请的信息到表单
requestForm.type = row.type
requestForm.name = row.name
if (row.type === 'docker') {
requestForm.dockerSource = row.dockerSource
requestForm.dockerImage = row.dockerImage
requestForm.envVars = [...row.envVars]
requestForm.ports = [...row.ports]
} else {
requestForm.windowsVersion = row.windowsVersion
requestForm.windowsImageUrl = row.windowsImageUrl
requestForm.activationMethod = row.activationMethod
}
requestForm.reason = row.reason
requestDialogVisible.value = true
}
// 关闭对话框
const handleDialogClose = () => {
requestDialogVisible.value = false
detailDialogVisible.value = false
if (requestFormRef.value) {
requestFormRef.value.resetFields()
}
}
// 提交表单
const submitForm = async () => {
if (!requestFormRef.value) return
await requestFormRef.value.validate((valid) => {
if (valid) {
// 这里应该是API请求
ElMessage.success('申请提交成功,请等待审核')
requestDialogVisible.value = false
fetchData()
}
})
}
// 初始加载
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.image-requests-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
/* 表单样式 */
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
/* 环境变量配置样式 */
.env-vars-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.env-vars-header {
display: flex;
margin-bottom: 10px;
font-weight: 600;
color: #606266;
}
.env-vars-item {
display: flex;
margin-bottom: 10px;
gap: 10px;
}
.env-var-name {
flex: 1;
}
.env-var-value {
flex: 1;
}
.env-var-action {
width: 40px;
}
.add-env-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 端口配置样式 */
.ports-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.ports-header {
display: flex;
margin-bottom: 10px;
font-weight: 600;
color: #606266;
}
.ports-item {
display: flex;
margin-bottom: 10px;
gap: 10px;
}
.port-container {
width: 150px;
}
.port-protocol {
width: 120px;
}
.port-desc {
flex: 1;
}
.port-action {
width: 40px;
}
.add-port-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 详情样式 */
.request-detail {
padding: 0 10px;
}
.request-reason {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
color: #606266;
line-height: 1.6;
}
.review-comment {
background-color: #f0f9eb;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
border-left: 4px solid #67c23a;
color: #606266;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>