feat:add guacamole
This commit is contained in:
@@ -35,6 +35,11 @@ export const menus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/acs/nodes', title: '节点管理'
|
path: '/acs/nodes', title: '节点管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/acs/guacamole',
|
||||||
|
title: '远程桌面网关管理',
|
||||||
|
icon: 'Monitor'
|
||||||
},{
|
},{
|
||||||
path: '/audit',
|
path: '/audit',
|
||||||
title: '站点审计',
|
title: '站点审计',
|
||||||
|
|||||||
@@ -130,6 +130,13 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '节点管理'
|
title: '节点管理'
|
||||||
}
|
}
|
||||||
|
},{
|
||||||
|
path: 'guacamole',
|
||||||
|
name: 'Guacamole',
|
||||||
|
component: () => import('../views/acs/guacamole/Guacamole.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'Guacamole管理'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {http2} from "@/utils/request.js";
|
||||||
|
|
||||||
|
/**获取 guacamole 列表 */
|
||||||
|
export const getGuacamoleList = data => {
|
||||||
|
return http2.get(`/v1/admin/server/get_guacamole_list`);
|
||||||
|
};
|
||||||
|
/**获取服务器 guacamole 信息 */
|
||||||
|
export const getGuacamoleInfo = data => {
|
||||||
|
return http2.get(`/v1/admin/server/get_server_guacamole?server_id=${data}`);
|
||||||
|
};
|
||||||
|
/**新增 guacamole 参数 url:string,username:string,password:string*/
|
||||||
|
export const addGuacamoleInfo = data => {
|
||||||
|
return http2.post(`/v1/admin/server/add_guacamole`, data,{
|
||||||
|
headers:{
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**修改guacamole 参数 id:string,url:string,username:string,password:string*/
|
||||||
|
export const updateGuacamoleInfo = data => {
|
||||||
|
return http2.post(`/v1/admin/server/edit_guacamole`, data,{
|
||||||
|
headers:{
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**删除guacamole 参数 id:string */
|
||||||
|
export const deleteGuacamoleInfo = data => {
|
||||||
|
return http2.post(`/v1/admin/server/delete_guacamole`, data,{
|
||||||
|
headers:{
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,739 @@
|
|||||||
|
<template>
|
||||||
|
<div class="guacamole-container">
|
||||||
|
<!-- 页面标题和操作按钮 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="left">
|
||||||
|
<h2 class="title">远程桌面网关管理</h2>
|
||||||
|
<el-tag type="info" effect="plain" class="count-tag">共 {{ guacamoleStats.total }} 个配置</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">添加配置</el-button>
|
||||||
|
<el-button @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-panel">
|
||||||
|
<div class="stat-card total-card">
|
||||||
|
<div class="stat-icon"><el-icon><Monitor /></el-icon></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ guacamoleStats.total }}</div>
|
||||||
|
<div class="stat-label">总配置数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card active-card">
|
||||||
|
<div class="stat-icon"><el-icon><CircleCheck /></el-icon></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ guacamoleStats.active }}</div>
|
||||||
|
<div class="stat-label">活跃配置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card error-card">
|
||||||
|
<div class="stat-icon"><el-icon><CircleClose /></el-icon></div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ guacamoleStats.error }}</div>
|
||||||
|
<div class="stat-label">异常配置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索和筛选 -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<el-input
|
||||||
|
v-model="filterForm.url"
|
||||||
|
placeholder="搜索 Guacamole URL"
|
||||||
|
prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
|
||||||
|
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guacamole 配置列表 -->
|
||||||
|
<div class="table-container">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="guacamoleData"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
table-layout="auto"
|
||||||
|
class="guacamole-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="密码" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="togglePasswordVisibility(scope.row)"
|
||||||
|
:icon="scope.row.showPassword ? View : Hide"
|
||||||
|
>
|
||||||
|
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="180" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-tooltip content="编辑配置" placement="top" :hide-after="1500">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
:icon="Edit"
|
||||||
|
circle
|
||||||
|
@click="handleEdit(scope.row)"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除配置" placement="top" :hide-after="1500">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑配置对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '添加 Guacamole 配置' : '编辑 Guacamole 配置'"
|
||||||
|
:width="dialogWidth"
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:before-close="handleDialogClose"
|
||||||
|
class="guacamole-dialog"
|
||||||
|
>
|
||||||
|
<el-form :model="guacamoleForm" label-width="120px" :rules="rules" ref="guacamoleFormRef">
|
||||||
|
<el-form-item label="Guacamole URL" prop="url">
|
||||||
|
<el-input
|
||||||
|
v-model="guacamoleForm.url"
|
||||||
|
placeholder="请输入 Guacamole 服务器地址 (如: http://192.168.1.100:8080/guacamole)"
|
||||||
|
/>
|
||||||
|
<div class="form-item-tip">请确保 URL 格式正确,包含协议、IP/域名和端口</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="管理员用户名" prop="username">
|
||||||
|
<el-input v-model="guacamoleForm.username" placeholder="请输入管理员用户名" />
|
||||||
|
<div class="form-item-tip">用于连接 Guacamole 服务的管理员账号</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="管理员密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="guacamoleForm.password"
|
||||||
|
placeholder="请输入管理员密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
<div class="form-item-tip">用于连接 Guacamole 服务的管理员密码</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="连接测试">
|
||||||
|
<el-button
|
||||||
|
@click="testConnection"
|
||||||
|
:loading="testingConnection"
|
||||||
|
:icon="Connection"
|
||||||
|
>
|
||||||
|
{{ testingConnection ? '测试中...' : '测试连接' }}
|
||||||
|
</el-button>
|
||||||
|
<div class="form-item-tip">建议在保存前测试连接是否正常</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<div class="left-actions">
|
||||||
|
<!-- 预留空间给可能的其他操作 -->
|
||||||
|
</div>
|
||||||
|
<div class="right-actions">
|
||||||
|
<el-button @click="handleDialogClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||||||
|
{{ submitting ? '保存中...' : '确认' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||||
|
import {
|
||||||
|
Plus, Refresh, Edit, Delete, Monitor, CircleCheck, CircleClose,
|
||||||
|
View, Hide, Connection, Search
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getGuacamoleList,
|
||||||
|
addGuacamoleInfo,
|
||||||
|
updateGuacamoleInfo,
|
||||||
|
deleteGuacamoleInfo
|
||||||
|
} from '@/utils/acs/guacamole'
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const guacamoleData = ref([])
|
||||||
|
|
||||||
|
// 筛选表单
|
||||||
|
const filterForm = reactive({
|
||||||
|
url: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilter = () => {
|
||||||
|
filterForm.url = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const guacamoleStats = reactive({
|
||||||
|
total: 0,
|
||||||
|
active: 0,
|
||||||
|
error: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
pagination.currentPage = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每页条数变化
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pagination.pageSize = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框相关
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref('add') // 'add', 'edit'
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 表单对象和规则
|
||||||
|
const guacamoleFormRef = ref(null)
|
||||||
|
const guacamoleForm = reactive({
|
||||||
|
id: '',
|
||||||
|
url: '',
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
url: [
|
||||||
|
{ required: true, message: '请输入 Guacamole URL', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^https?:\/\/.+/,
|
||||||
|
message: '请输入有效的URL地址 (以http://或https://开头)',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 50, message: '用户名长度应为2-50个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度至少为6个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接测试
|
||||||
|
const testingConnection = ref(false)
|
||||||
|
|
||||||
|
const testConnection = async () => {
|
||||||
|
if (!guacamoleForm.url || !guacamoleForm.username || !guacamoleForm.password) {
|
||||||
|
ElMessage.warning('请先填写完整的连接信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testingConnection.value = true
|
||||||
|
try {
|
||||||
|
// 这里可以添加实际的连接测试逻辑
|
||||||
|
// 暂时模拟测试过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
ElMessage.success('连接测试成功!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('连接测试失败:', error)
|
||||||
|
ElMessage.error('连接测试失败,请检查配置信息')
|
||||||
|
} finally {
|
||||||
|
testingConnection.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const handleRefresh = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: '刷新中',
|
||||||
|
message: '正在重新获取 Guacamole 配置数据',
|
||||||
|
type: 'info',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getGuacamoleList()
|
||||||
|
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
const data = res.data.data || []
|
||||||
|
|
||||||
|
// 为每个项目添加密码显示状态
|
||||||
|
guacamoleData.value = data.map(item => ({
|
||||||
|
...item,
|
||||||
|
showPassword: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
updateStats()
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
pagination.total = data.length
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取 Guacamole 配置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 Guacamole 配置失败:', error)
|
||||||
|
ElMessage.error('获取 Guacamole 配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
const updateStats = () => {
|
||||||
|
guacamoleStats.total = guacamoleData.value.length
|
||||||
|
guacamoleStats.active = guacamoleData.value.filter(item => item.status !== 'error').length
|
||||||
|
guacamoleStats.error = guacamoleData.value.filter(item => item.status === 'error').length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换密码显示状态
|
||||||
|
const togglePasswordVisibility = (row) => {
|
||||||
|
row.showPassword = !row.showPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加配置
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
dialogVisible.value = true
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
Object.keys(guacamoleForm).forEach(key => {
|
||||||
|
guacamoleForm[key] = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 确保在下一个 tick 重置表单验证状态
|
||||||
|
nextTick(() => {
|
||||||
|
if (guacamoleFormRef.value) {
|
||||||
|
guacamoleFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑配置
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
dialogVisible.value = true
|
||||||
|
|
||||||
|
// 填充表单
|
||||||
|
Object.keys(guacamoleForm).forEach(key => {
|
||||||
|
if (key in row) {
|
||||||
|
guacamoleForm[key] = row[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (guacamoleFormRef.value) {
|
||||||
|
guacamoleFormRef.value.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要删除配置"${row.url}"吗?`,
|
||||||
|
'删除确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error',
|
||||||
|
draggable: true,
|
||||||
|
distinguishCancelAndClose: true,
|
||||||
|
closeOnClickModal: false
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteGuacamoleInfo({ id: row.id })
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
ElNotification({
|
||||||
|
title: '删除成功',
|
||||||
|
message: `配置"${row.url}"已被成功删除`,
|
||||||
|
type: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('删除失败!')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除配置失败:', error)
|
||||||
|
ElMessage.error('删除配置失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
const handleDialogClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
if (guacamoleFormRef.value) {
|
||||||
|
guacamoleFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!guacamoleFormRef.value) return
|
||||||
|
|
||||||
|
await guacamoleFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const formData = { ...guacamoleForm }
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
let data = {
|
||||||
|
url:formData.url,
|
||||||
|
username:formData.username,
|
||||||
|
password:formData.password
|
||||||
|
}
|
||||||
|
res = await addGuacamoleInfo(data)
|
||||||
|
} else {
|
||||||
|
res = await updateGuacamoleInfo(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res && res.data && res.data.code === 200) {
|
||||||
|
ElNotification({
|
||||||
|
title: dialogType.value === 'add' ? '添加成功' : '更新成功',
|
||||||
|
message: `Guacamole 配置已${dialogType.value === 'add' ? '添加' : '更新'}成功`,
|
||||||
|
type: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
const errorMsg = res?.data?.msg || '操作失败'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交表单失败:', error)
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('请完善表单信息')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算对话框宽度
|
||||||
|
const dialogWidth = computed(() => {
|
||||||
|
const width = window.innerWidth
|
||||||
|
if (width < 768) return '95%'
|
||||||
|
if (width < 992) return '80%'
|
||||||
|
return '50%'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化失败:', error)
|
||||||
|
ElMessage.error('页面初始化失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.guacamole-container {
|
||||||
|
padding: 20px;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面标题样式 */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-tag {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-card .stat-icon {
|
||||||
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-card .stat-icon {
|
||||||
|
background-color: rgba(103, 194, 58, 0.1);
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card .stat-icon {
|
||||||
|
background-color: rgba(245, 108, 108, 0.1);
|
||||||
|
color: #F56C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索和筛选部分 */
|
||||||
|
.filter-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格容器 */
|
||||||
|
.table-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guacamole-table {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 */
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.guacamole-dialog :deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed #ebeef5;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框底部 */
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-actions, .right-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.stats-panel {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:last-child {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -201,7 +201,31 @@
|
|||||||
|
|
||||||
<div class="form-section-title">认证与展示</div>
|
<div class="form-section-title">认证与展示</div>
|
||||||
<el-form-item v-if="serverForm.server_type === 'dockerContainer'" label="Auth-ID">
|
<el-form-item v-if="serverForm.server_type === 'dockerContainer'" label="Auth-ID">
|
||||||
<el-input v-model="serverForm.auth_id" placeholder="节点服务器管理员的auth_id" />
|
<el-input v-model="serverForm.auth_id" placeholder="服务器管理id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="Guacamole-ID">
|
||||||
|
<el-input v-model="serverForm.guacamole_id" placeholder="guacamole服务id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="登录用户名">
|
||||||
|
<el-input v-model="serverForm.username" placeholder="服务器登录用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="登录密码">
|
||||||
|
<el-input
|
||||||
|
v-model="serverForm.password"
|
||||||
|
placeholder="服务器登录密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="端口映射">
|
||||||
|
<el-switch
|
||||||
|
v-model="serverForm.allow_port_forward"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="开启"
|
||||||
|
inactive-text="关闭"
|
||||||
|
/>
|
||||||
|
<div class="form-item-description">服务器是否开放端口映射</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Token">
|
<el-form-item label="Token">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -335,6 +359,15 @@ const rules = {
|
|||||||
server_ip: [
|
server_ip: [
|
||||||
{ required: true, message: '请输入IP地址', trigger: 'blur' },
|
{ required: true, message: '请输入IP地址', trigger: 'blur' },
|
||||||
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
|
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
guacamole_id: [
|
||||||
|
{ required: false, message: '请输入Guacamole服务ID', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{ required: false, message: '请输入登录用户名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: false, message: '请输入登录密码', trigger: 'blur' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +451,8 @@ const handleAdd = () => {
|
|||||||
Object.keys(serverForm).forEach(key => {
|
Object.keys(serverForm).forEach(key => {
|
||||||
if (key === 'hide') {
|
if (key === 'hide') {
|
||||||
serverForm[key] = 0
|
serverForm[key] = 0
|
||||||
|
} else if (key === 'allow_port_forward') {
|
||||||
|
serverForm[key] = 0
|
||||||
} else if (key === 'server_type') {
|
} else if (key === 'server_type') {
|
||||||
serverForm[key] = serverType.value
|
serverForm[key] = serverType.value
|
||||||
} else {
|
} else {
|
||||||
@@ -534,7 +569,7 @@ const submitForm = async () => {
|
|||||||
const formData = { ...serverForm }
|
const formData = { ...serverForm }
|
||||||
|
|
||||||
// 转换可能的数字字段
|
// 转换可能的数字字段
|
||||||
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide'];
|
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward'];
|
||||||
numericFields.forEach(field => {
|
numericFields.forEach(field => {
|
||||||
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
|
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
|
||||||
formData[field] = Number(formData[field])
|
formData[field] = Number(formData[field])
|
||||||
@@ -985,6 +1020,13 @@ onMounted(async () => {
|
|||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-item-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user