feat: 对接主控服务接口
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user