feat:add guacamole
This commit is contained in:
@@ -35,6 +35,11 @@ export const menus = [
|
||||
},
|
||||
{
|
||||
path: '/acs/nodes', title: '节点管理'
|
||||
},
|
||||
{
|
||||
path: '/acs/guacamole',
|
||||
title: '远程桌面网关管理',
|
||||
icon: 'Monitor'
|
||||
},{
|
||||
path: '/audit',
|
||||
title: '站点审计',
|
||||
|
||||
@@ -130,6 +130,13 @@ const routes = [
|
||||
meta: {
|
||||
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>
|
||||
<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 label="Token">
|
||||
<el-input
|
||||
@@ -335,6 +359,15 @@ const rules = {
|
||||
server_ip: [
|
||||
{ required: true, 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 => {
|
||||
if (key === 'hide') {
|
||||
serverForm[key] = 0
|
||||
} else if (key === 'allow_port_forward') {
|
||||
serverForm[key] = 0
|
||||
} else if (key === 'server_type') {
|
||||
serverForm[key] = serverType.value
|
||||
} else {
|
||||
@@ -534,7 +569,7 @@ const submitForm = async () => {
|
||||
const formData = { ...serverForm }
|
||||
|
||||
// 转换可能的数字字段
|
||||
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide'];
|
||||
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward'];
|
||||
numericFields.forEach(field => {
|
||||
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
|
||||
formData[field] = Number(formData[field])
|
||||
@@ -985,6 +1020,13 @@ onMounted(async () => {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.form-item-description {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user