feat: 对接主控服务接口
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<div class="kvm-detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<el-button @click="goBack" link class="back-btn">
|
||||
<el-icon><ArrowLeft /></el-icon> 返回列表
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="page-title">主控服务详情</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" plain @click="refreshData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon> 刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content" v-loading="loading">
|
||||
<!-- 服务概览卡片 -->
|
||||
<el-card class="profile-card" shadow="never">
|
||||
<div class="profile-header">
|
||||
<div class="profile-basic">
|
||||
<div class="service-icon-wrapper">
|
||||
<el-icon :size="48" color="#409eff"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="service-identity">
|
||||
<div class="name-row">
|
||||
<h1 class="service-name">{{ serviceInfo.Name || serviceInfo.name || '未命名服务' }}</h1>
|
||||
<el-tag type="success" effect="dark" round size="small" class="status-tag">运行中</el-tag>
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<span class="label">ID:</span>
|
||||
<span class="value">{{ serviceInfo.Id ?? serviceInfo.id }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="label">地址:</span>
|
||||
<span class="value addr-value">{{ serviceInfo.Host || serviceInfo.host }}:{{ serviceInfo.Port || serviceInfo.port }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="label">创建:</span>
|
||||
<span class="value">{{ formatTime(serviceInfo.CreatedAt || serviceInfo.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 数据概览 -->
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">认证Token</div>
|
||||
<div class="stat-value">
|
||||
<el-tag v-if="serviceInfo.Token || serviceInfo.token" type="success" size="small">已设置</el-tag>
|
||||
<el-tag v-else type="info" size="small">未设置</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">备注</div>
|
||||
<div class="stat-value note-value">{{ serviceInfo.Note || serviceInfo.note || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider class="action-divider" />
|
||||
|
||||
<!-- 快捷操作栏 -->
|
||||
<div class="quick-actions">
|
||||
<el-button type="primary" plain :icon="Edit" @click="handleEditService">编辑服务</el-button>
|
||||
<el-button type="danger" plain :icon="Delete" @click="handleDeleteService">删除服务</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 子模块Tab -->
|
||||
<el-card class="tabs-card" shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="custom-tabs">
|
||||
<el-tab-pane label="宿主机组映射" name="host-group">
|
||||
<HostGroupMapping v-if="tabLoaded['host-group']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="远程宿主机组" name="remote-host-group">
|
||||
<RemoteHostGroupManage v-if="tabLoaded['remote-host-group']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="宿主机管理" name="host">
|
||||
<HostManage v-if="tabLoaded['host']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="镜像管理" name="image">
|
||||
<ImageManage v-if="tabLoaded['image']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="网络管理" name="network">
|
||||
<NetworkManage v-if="tabLoaded['network']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="数据卷管理" name="volume">
|
||||
<VolumeManage v-if="tabLoaded['volume']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="虚拟机管理" name="vm">
|
||||
<VmManage v-if="tabLoaded['vm']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="安全组管理" name="security">
|
||||
<SecurityGroupManage v-if="tabLoaded['security']" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="VNC节点" name="vnc">
|
||||
<VncNodeManage v-if="tabLoaded['vnc']" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 编辑服务弹窗 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑主控服务" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="服务名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入服务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务地址" prop="host">
|
||||
<el-input v-model="formData.host" placeholder="请输入服务地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务端口" prop="port">
|
||||
<el-input v-model="formData.port" placeholder="请输入服务端口" />
|
||||
</el-form-item>
|
||||
<el-form-item label="认证Token" prop="token">
|
||||
<el-input v-model="formData.token" placeholder="认证Token(可选)" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="formData.note" type="textarea" :rows="3" placeholder="备注说明(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmitEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, provide, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getKvmServiceDetail, updateKvmService, deleteKvmService
|
||||
} from '@/api/admin/kvmService'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 子模块组件(懒加载)
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
const HostGroupMapping = defineAsyncComponent(() => import('./HostGroupMapping.vue'))
|
||||
const HostManage = defineAsyncComponent(() => import('./HostManage.vue'))
|
||||
const ImageManage = defineAsyncComponent(() => import('./ImageManage.vue'))
|
||||
const NetworkManage = defineAsyncComponent(() => import('./NetworkManage.vue'))
|
||||
const VolumeManage = defineAsyncComponent(() => import('./VolumeManage.vue'))
|
||||
const VmManage = defineAsyncComponent(() => import('./VmManage.vue'))
|
||||
const SecurityGroupManage = defineAsyncComponent(() => import('./SecurityGroupManage.vue'))
|
||||
const VncNodeManage = defineAsyncComponent(() => import('./VncNodeManage.vue'))
|
||||
const RemoteHostGroupManage = defineAsyncComponent(() => import('./RemoteHostGroupManage.vue'))
|
||||
|
||||
// 引入tagsViewStore
|
||||
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const serviceId = computed(() => parseInt(route.query.service_id) || 0)
|
||||
const serviceName = computed(() => route.query.service_name || '')
|
||||
|
||||
// 向子组件注入上下文 —— 子组件通过 inject 获取
|
||||
provide('embedded', true)
|
||||
provide('serviceId', serviceId)
|
||||
provide('serviceName', serviceName)
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const serviceInfo = ref({})
|
||||
|
||||
// Tab 管理
|
||||
const activeTab = ref('host-group')
|
||||
const tabLoaded = reactive({
|
||||
'host-group': true, // 默认加载第一个
|
||||
'remote-host-group': false,
|
||||
'host': false,
|
||||
'image': false,
|
||||
'network': false,
|
||||
'volume': false,
|
||||
'vm': false,
|
||||
'security': false,
|
||||
'vnc': false
|
||||
})
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
const name = tab.props.name
|
||||
if (!tabLoaded[name]) {
|
||||
tabLoaded[name] = true
|
||||
}
|
||||
localStorage.setItem('kvmDetailActiveTab', name)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (t) => {
|
||||
if (!t) return '-'
|
||||
return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 加载服务详情
|
||||
const fetchServiceInfo = async () => {
|
||||
if (!serviceId.value) {
|
||||
ElMessage.error('服务ID不能为空')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getKvmServiceDetail({ id: serviceId.value })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
serviceInfo.value = body.data
|
||||
} else {
|
||||
ElMessage.error(body?.message || '获取服务详情失败')
|
||||
// 使用 query 参数中的基本信息做兜底
|
||||
serviceInfo.value = { Id: serviceId.value, Name: serviceName.value }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取服务详情失败:', error)
|
||||
serviceInfo.value = { Id: serviceId.value, Name: serviceName.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有数据
|
||||
const refreshData = () => {
|
||||
fetchServiceInfo()
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const goBack = () => {
|
||||
tagsViewStore.delVisitedView(route)
|
||||
router.push('/virtualization/kvm-service')
|
||||
}
|
||||
|
||||
// 编辑服务
|
||||
const editDialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formData = reactive({ name: '', host: '', port: '', token: '', note: '' })
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入服务名称', trigger: 'blur' }],
|
||||
host: [{ required: true, message: '请输入服务地址', trigger: 'blur' }],
|
||||
port: [{ required: true, message: '请输入服务端口', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleEditService = () => {
|
||||
Object.assign(formData, {
|
||||
name: serviceInfo.value.Name ?? serviceInfo.value.name ?? '',
|
||||
host: serviceInfo.value.Host ?? serviceInfo.value.host ?? '',
|
||||
port: serviceInfo.value.Port ?? serviceInfo.value.port ?? '',
|
||||
token: serviceInfo.value.Token ?? serviceInfo.value.token ?? '',
|
||||
note: serviceInfo.value.Note ?? serviceInfo.value.note ?? ''
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmitEdit = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await updateKvmService(serviceId.value, {
|
||||
name: formData.name, host: formData.host, port: formData.port,
|
||||
token: formData.token, note: formData.note
|
||||
})
|
||||
const body = res?.data
|
||||
if (body?.code === 200) {
|
||||
ElMessage.success('更新成功')
|
||||
editDialogVisible.value = false
|
||||
fetchServiceInfo()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '更新失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('更新失败: ' + (e?.response?.data?.message || e.message))
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
// 删除服务
|
||||
const handleDeleteService = () => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除主控服务「${serviceInfo.value.Name || serviceInfo.value.name}」吗?删除后不可恢复。`,
|
||||
'删除确认',
|
||||
{ confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' }
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await deleteKvmService({ id: serviceId.value })
|
||||
const body = res?.data
|
||||
if (body?.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
goBack()
|
||||
} else {
|
||||
ElMessage.error(body?.message || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchServiceInfo()
|
||||
// 恢复上次选中的 Tab
|
||||
const savedTab = localStorage.getItem('kvmDetailActiveTab')
|
||||
if (savedTab && tabLoaded.hasOwnProperty(savedTab)) {
|
||||
activeTab.value = savedTab
|
||||
tabLoaded[savedTab] = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kvm-detail-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 概览卡片 */
|
||||
.profile-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.profile-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.service-icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #e8f4fd, #d6eaff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.service-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.id-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.id-row .label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.id-row .value {
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.addr-value {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.note-value {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-divider {
|
||||
margin: 16px 0 12px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Tab 卡片 */
|
||||
.tabs-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-tabs :deep(.el-tabs__item) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.custom-tabs :deep(.el-tab-pane) {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 嵌入子模块时去掉内边距 */
|
||||
.custom-tabs :deep(.page-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-tabs :deep(.host-group-mapping-container),
|
||||
.custom-tabs :deep(.remote-hg-container),
|
||||
.custom-tabs :deep(.host-manage-container),
|
||||
.custom-tabs :deep(.image-manage-container),
|
||||
.custom-tabs :deep(.network-manage-container),
|
||||
.custom-tabs :deep(.volume-manage-container),
|
||||
.custom-tabs :deep(.vm-manage-container),
|
||||
.custom-tabs :deep(.sg-manage-container),
|
||||
.custom-tabs :deep(.vnc-node-container) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user