Files
ApiServer-Web-admin_dashboa…/src/views/acs/guacamole/Guacamole.vue
T
wlkjyy f7c3be1d30 refactor: extract image form to standalone page and implement tags view store
- Created ImageForm.vue as standalone page for add/edit image functionality
- Removed dialog-based image form from VmImages.vue
- Implemented tagsViewStore for global tab state management
- Added automatic tab closing on form cancel/back
- Fixed data persistence issue when switching between image edits
- Removed quick actions section from ImageForm
- Updated router configuration for new image form route
2025-11-28 14:15:29 +08:00

727 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="guacamole-container">
<!-- 统计卡片 -->
<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>
<el-card class="main-container" shadow="never">
<!-- 搜索和筛选 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="filterForm" class="search-form">
<el-form-item>
<el-input
v-model="filterForm.url"
placeholder="搜索 Guacamole URL"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
style="width: 300px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetFilter">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>添加配置
</el-button>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- Guacamole 配置列表 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="guacamoleData"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<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="primary"
link
size="small"
@click="togglePasswordVisibility(scope.row)"
>
<el-icon style="margin-right: 4px"><component :is="scope.row.showPassword ? View : Hide" /></el-icon>
{{ 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">
<el-button
type="primary"
link
@click="handleEdit(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<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
class="pagination"
/>
</div>
</el-card>
<!-- 添加/编辑配置对话框 -->
<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: 0;
}
/* 统计卡片 */
.stats-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px;
display: flex;
align-items: center;
transition: all 0.3s;
border: 1px solid #e1e8ed;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px 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;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
/* 表单样式 */
.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;
}
/* 对话框底部 */
.dialog-footer {
display: flex;
justify-content: space-between;
width: 100%;
}
.left-actions, .right-actions {
display: flex;
gap: 8px;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* 响应式设计 */
@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) {
.stats-panel {
grid-template-columns: 1fr;
}
.stat-card:last-child {
grid-column: auto;
}
.filter-content {
flex-direction: column;
align-items: stretch;
}
.search-form {
width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
}
}
</style>