feat: 对接主控服务接口
Build and Deploy Vue3 / build (push) Successful in 2m29s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s

This commit is contained in:
lin
2026-03-14 15:45:07 +08:00
parent 25975c8b29
commit f4dbf17ce9
21 changed files with 6323 additions and 67 deletions
+352
View File
@@ -0,0 +1,352 @@
<template>
<div class="vnc-node-container">
<div class="page-header" v-if="!embedded">
<div class="header-left">
<el-button @click="goBack" :icon="ArrowLeft">返回</el-button>
<div class="header-info">
<h3>VNC 节点管理</h3>
<span class="sub-info" v-if="serviceName">主控服务{{ serviceName }}</span>
</div>
</div>
<div class="header-right">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>新增节点</el-button>
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
</div>
</div>
<!-- 嵌入模式下的工具栏 -->
<div class="embedded-toolbar" v-if="embedded">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>新增节点</el-button>
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
<el-input v-model="keyword" placeholder="搜索节点" clearable style="width: 220px; margin-left: auto;" @keyup.enter="handleSearch" @clear="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
</div>
<!-- 独立模式下的筛选栏 -->
<div class="filter-bar" v-if="!embedded">
<el-input v-model="keyword" placeholder="搜索VNC节点" clearable style="width: 220px" @keyup.enter="handleSearch" @clear="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
</div>
<!-- 节点列表 -->
<el-table :data="nodeList" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
<el-table-column label="地址" min-width="200">
<template #default="{ row }">
<span class="host-addr">{{ row.ip || '-' }}:{{ row.port || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="Token" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.token" class="token-mask">{{ maskToken(row.token) }}</span>
<span v-else class="text-muted">未设置</span>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="success" @click="handleTest(row)">测试连接</el-button>
<el-button link type="primary" @click="handleVmVnc(row)">VM VNC</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" v-if="total > queryParams.page_size">
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.page_size"
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next"
@size-change="s => { queryParams.page_size = s; queryParams.page = 1; loadList() }"
@current-change="p => { queryParams.page = p; loadList() }" />
</div>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="formDialogVisible" :title="formType === 'add' ? '新增 VNC 节点' : '编辑 VNC 节点'" 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="VNC节点名称" />
</el-form-item>
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder=" 192.168.1.100" />
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input v-model="formData.port" placeholder=" 6080" />
</el-form-item>
<el-form-item label="Token">
<el-input v-model="formData.token" placeholder="认证Token可选" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 测试连接弹窗 -->
<el-dialog v-model="testDialogVisible" title="测试 VNC 节点连接" width="460px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="VNC节点">{{ testTarget?.name || '-' }}</el-form-item>
<el-form-item label="宿主机">
<el-select v-model="testHostId" placeholder="选择宿主机" filterable style="width: 100%">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
</el-form-item>
</el-form>
<div v-if="testResult" class="test-result" :class="testResult.success ? 'success' : 'error'">
<el-icon v-if="testResult.success"><SuccessFilled /></el-icon>
<el-icon v-else><CircleCloseFilled /></el-icon>
{{ testResult.message }}
</div>
<template #footer>
<el-button @click="testDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="testLoading" @click="submitTest">测试</el-button>
</template>
</el-dialog>
<!-- VM VNC 弹窗 -->
<el-dialog v-model="vmVncDialogVisible" title="获取虚拟机 VNC 连接" width="520px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="VNC节点">{{ vmVncTarget?.name || '-' }}</el-form-item>
<el-form-item label="虚拟机">
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<el-input :model-value="vmVncVmId ? `${vmVncVmName || ''} (ID: ${vmVncVmId})` : ''" readonly placeholder="请选择虚拟机" style="flex: 1" />
<el-button type="primary" @click="showVmVncSelector = true">选择</el-button>
</div>
</el-form-item>
</el-form>
<div v-if="vmVncResult" class="vnc-result">
<el-descriptions :column="1" border size="small">
<el-descriptions-item v-for="(val, key) in vmVncResult" :key="key" :label="key">
<template v-if="typeof val === 'string' && (val.startsWith('http') || val.startsWith('ws'))">
<el-link type="primary" :href="val" target="_blank">{{ val }}</el-link>
</template>
<template v-else>{{ val }}</template>
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="vmVncDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="vmVncLoading" @click="submitVmVnc">获取</el-button>
</template>
</el-dialog>
<!-- 虚拟机选择器 -->
<VmSelectorPopup v-model="showVmVncSelector" :service-id="serviceId" :current-id="vmVncVmId" @confirm="handleVmVncSelected" />
</div>
</template>
<script setup>
import { ref, reactive, computed, inject, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, ArrowLeft, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import {
getVncNodeList, getVmVnc, addVncNode, testVncNode, updateVncNode, deleteVncNode,
getRemoteHostList
} from '@/api/admin/kvmService'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
const route = useRoute()
const router = useRouter()
const embedded = inject('embedded', false)
const injectedServiceId = inject('serviceId', null)
const injectedServiceName = inject('serviceName', null)
const serviceId = computed(() => injectedServiceId?.value || parseInt(route.query.service_id) || 0)
const serviceName = computed(() => injectedServiceName?.value || route.query.service_name || '')
const loading = ref(false)
const submitLoading = ref(false)
const nodeList = ref([])
const total = ref(0)
const keyword = ref('')
const hostOptions = ref([])
const queryParams = reactive({ page: 1, page_size: 10 })
const maskToken = (token) => {
if (!token) return ''
if (token.length <= 8) return '****'
return token.substring(0, 4) + '****' + token.substring(token.length - 4)
}
const loadHostOptions = async () => {
try {
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 100 })
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
hostOptions.value = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
}
} catch (e) { console.error('加载宿主机列表失败:', e) }
}
const loadList = async () => {
if (!serviceId.value) return
loading.value = true
try {
const params = { service_id: serviceId.value, page: queryParams.page, page_size: queryParams.page_size }
if (keyword.value) params.keyword = keyword.value
const res = await getVncNodeList(params)
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
nodeList.value = inner.items || inner.vnc_nodes || inner.nodes || inner.data || (Array.isArray(inner) ? inner : [])
total.value = inner.total ?? inner.all_count ?? nodeList.value.length
} else { nodeList.value = []; total.value = 0 }
} catch (e) {
console.error('获取VNC节点列表失败:', e)
ElMessage.error('获取VNC节点列表失败')
} finally { loading.value = false }
}
const handleSearch = () => { queryParams.page = 1; loadList() }
// 表单
const formDialogVisible = ref(false)
const formType = ref('add')
const formRef = ref(null)
const formData = reactive({ id: undefined, name: '', ip: '', port: '', token: '' })
const formRules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
port: [{ required: true, message: '请输入端口', trigger: 'blur' }]
}
const handleAdd = () => {
formType.value = 'add'
Object.assign(formData, { id: undefined, name: '', ip: '', port: '', token: '' })
formDialogVisible.value = true
}
const handleEdit = (row) => {
formType.value = 'edit'
Object.assign(formData, {
id: row.id, name: row.name || '', ip: row.ip || '', port: row.port || '', token: row.token || ''
})
formDialogVisible.value = true
}
const handleSubmit = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
let res
if (formType.value === 'add') {
res = await addVncNode({ service_id: serviceId.value, name: formData.name, ip: formData.ip, port: formData.port, token: formData.token })
} else {
res = await updateVncNode({ service_id: serviceId.value, id: formData.id, name: formData.name, ip: formData.ip, port: formData.port, token: formData.token })
}
if (res?.data?.code === 200) {
ElMessage.success(formType.value === 'add' ? '创建成功' : '修改成功')
formDialogVisible.value = false
loadList()
} else ElMessage.error(res?.data?.message || '操作失败')
} catch (e) { ElMessage.error('操作失败') } finally { submitLoading.value = false }
})
}
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除VNC节点「${row.name}」吗?`, '删除确认', {
confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteVncNode({ service_id: serviceId.value, id: row.id })
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadList() }
else ElMessage.error(res?.data?.message || '删除失败')
} catch (e) { ElMessage.error('删除失败') }
}).catch(() => {})
}
// 测试连接
const testDialogVisible = ref(false)
const testTarget = ref(null)
const testHostId = ref(null)
const testLoading = ref(false)
const testResult = ref(null)
const handleTest = (row) => {
testTarget.value = row
testHostId.value = null
testResult.value = null
testDialogVisible.value = true
}
const submitTest = async () => {
if (!testHostId.value) { ElMessage.warning('请选择宿主机'); return }
testLoading.value = true
testResult.value = null
try {
const res = await testVncNode({ service_id: serviceId.value, id: testTarget.value.id, host_id: testHostId.value })
if (res?.data?.code === 200) {
testResult.value = { success: true, message: '连接测试成功' }
} else {
testResult.value = { success: false, message: res?.data?.message || '连接测试失败' }
}
} catch (e) {
testResult.value = { success: false, message: '连接测试异常: ' + (e?.message || '未知错误') }
} finally { testLoading.value = false }
}
// VM VNC
const vmVncDialogVisible = ref(false)
const vmVncTarget = ref(null)
const vmVncVmId = ref(0)
const vmVncVmName = ref('')
const vmVncLoading = ref(false)
const vmVncResult = ref(null)
const showVmVncSelector = ref(false)
const handleVmVncSelected = (vm) => { vmVncVmId.value = vm.id; vmVncVmName.value = vm.name || '' }
const handleVmVnc = (row) => {
vmVncTarget.value = row
vmVncVmId.value = 0
vmVncVmName.value = ''
vmVncResult.value = null
vmVncDialogVisible.value = true
}
const submitVmVnc = async () => {
if (!vmVncVmId.value) { ElMessage.warning('请选择虚拟机'); return }
vmVncLoading.value = true
vmVncResult.value = null
try {
const res = await getVmVnc({ service_id: serviceId.value, id: vmVncTarget.value.id, vm_id: vmVncVmId.value })
if (res?.data?.code === 200 && res?.data?.data) {
vmVncResult.value = res.data.data
} else {
ElMessage.error(res?.data?.message || '获取VNC连接信息失败')
}
} catch (e) { ElMessage.error('获取失败') } finally { vmVncLoading.value = false }
}
const goBack = () => { router.push('/virtualization/kvm-service') }
onMounted(async () => {
if (serviceId.value) {
await loadHostOptions()
loadList()
}
})
</script>
<style scoped>
.vnc-node-container { padding: 20px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #ebeef5; }
.header-left { display: flex; align-items: center; gap: 16px; }
.header-info h3 { margin: 0; font-size: 18px; color: #303133; }
.sub-info { font-size: 13px; color: #909399; }
.header-right { display: flex; gap: 8px; }
.embedded-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
.host-addr { font-family: 'Consolas', 'Monaco', monospace; color: #409eff; font-size: 13px; }
.token-mask { font-family: 'Consolas', 'Monaco', monospace; color: #909399; font-size: 13px; }
.text-muted { color: #c0c4cc; }
.test-result { display: flex; align-items: center; gap: 8px; padding: 12px; border-radius: 4px; margin-top: 12px; font-size: 14px; }
.test-result.success { background: #f0f9eb; color: #67c23a; }
.test-result.error { background: #fef0f0; color: #f56c6c; }
.vnc-result { margin-top: 12px; }
:deep(.el-table) { --el-table-header-bg-color: #fafafa; }
:deep(.el-table th) { font-weight: 600; color: #303133; font-size: 13px; }
</style>