Files
ApiServer-Web-admin_dashboa…/src/views/acs/nodes/Nodes.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

605 lines
15 KiB
Vue

<template>
<div class="nodes-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">{{ serverStats.total }}</div>
<div class="stat-label">总服务器数</div>
</div>
</div>
<div class="stat-card online-card">
<div class="stat-icon"><el-icon><CircleCheck /></el-icon></div>
<div class="stat-content">
<div class="stat-value">{{ serverStats.online }}</div>
<div class="stat-label">在线服务器</div>
</div>
</div>
<div class="stat-card offline-card">
<div class="stat-icon"><el-icon><CircleClose /></el-icon></div>
<div class="stat-content">
<div class="stat-value">{{ serverStats.offline }}</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-radio-group v-model="serverType" size="default" class="server-type-selector">
<el-radio-button value="dockerContainer">
<el-icon><Monitor /></el-icon> 容器云服务器
</el-radio-button>
<el-radio-button value="hyperV">
<el-icon><cpu /></el-icon> 虚拟机云服务器
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-input
v-model="filterForm.name"
placeholder="搜索服务器名称"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</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>
<!-- 服务器列表 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="serverData"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="server_id" label="ID" min-width="80" show-overflow-tooltip />
<el-table-column prop="name" label="服务器名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="server_ip" label="IP地址" min-width="120" show-overflow-tooltip />
<el-table-column label="状态" width="100" align="center">
<template #default="scope">
<div class="status-tag">
<span class="status-dot" :class="{ 'online': scope.row.state == 1, 'offline': scope.row.state != 1 }"></span>
<span>{{ scope.row.state == 1 ? '在线' : '离线' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="购物车展示" width="120" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.hide"
:active-value="0"
:inactive-value="1"
@change="handleVisibilityChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="scope">
<el-tooltip content="管理服务器" placement="top" :hide-after="1500">
<el-button
type="primary"
link
@click="handleManage(scope.row)"
>
<el-icon><Menu /></el-icon>管理
</el-button>
</el-tooltip>
<el-tooltip content="编辑服务器" placement="top" :hide-after="1500">
<el-button
type="warning"
link
@click="handleEdit(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
</el-tooltip>
<el-tooltip content="删除服务器" placement="top" :hide-after="1500">
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
>
<el-icon><delete /></el-icon>删除
</el-button>
</el-tooltip>
</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>
<!-- 添加/编辑服务器对话框 -->
<!-- <ServerDialog
v-model:visible="dialogVisible"
:type="dialogType"
:initial-data="currentServerData"
:default-type="serverType"
@success="fetchData"
/> -->
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
Plus, Refresh, Search, Edit, Delete, Menu,
Monitor, CircleCheck, CircleClose, Cpu
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { getServer, deleteServer, getServerStatus, editServer } from '@/utils/acs/server'
// import ServerDialog from './components/ServerDialog.vue'
const router = useRouter()
// 服务器类型
const serverType = ref('dockerContainer')
// 筛选表单
const filterForm = reactive({
name: ''
})
// 重置筛选
const resetFilter = () => {
filterForm.name = ''
handleSearch()
}
// 表格数据
const loading = ref(false)
const serverData = ref([])
// 服务器统计数据
const serverStats = reactive({
total: 0,
online: 0,
offline: 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 currentServerData = ref({})
// 处理搜索
const handleSearch = () => {
pagination.currentPage = 1
fetchData()
}
// 刷新数据
const handleRefresh = () => {
ElNotification({
title: '刷新中',
message: '正在重新获取服务器数据',
type: 'info',
duration: 2000
})
fetchData()
}
// 获取数据
const fetchData = async () => {
loading.value = true
try {
const res = await getServer(
pagination.currentPage,
pagination.pageSize,
filterForm.name || '',
serverType.value
)
if (res && res.data) {
serverData.value = res.data.data || []
pagination.total = res.data.count || 0
// 更新统计数据
updateStats()
// 获取服务器状态
const statusPromises = serverData.value.map(server =>
getServerStatus(server.server_id)
.then(statusRes => {
// 这里可以更新服务器状态,如果API返回了状态信息
return { id: server.server_id, success: true, data: statusRes?.data }
})
.catch(err => {
console.error(`获取服务器 ${server.server_id} 状态失败:`, err)
return { id: server.server_id, success: false, error: err }
})
)
Promise.allSettled(statusPromises).then(results => {
// 统计服务器状态获取情况
const failedCount = results.filter(r => !r.value?.success).length
if (failedCount > 0 && serverData.value.length > 0) {
// ElMessage.warning(`${failedCount}台服务器状态获取失败,可能需要检查连接`)
}
})
}
} catch (error) {
console.error('获取服务器列表失败:', error)
ElMessage.error('获取服务器列表失败')
} finally {
loading.value = false
}
}
// 更新统计数据
const updateStats = () => {
serverStats.total = pagination.total || serverData.value.length
serverStats.online = serverData.value.filter(server => server.state === 1).length
serverStats.offline = serverData.value.length - serverStats.online
}
// 添加服务器
const handleAdd = () => {
router.push({
path: '/acs/nodes/form',
query: { type: serverType.value }
})
}
// 编辑服务器
const handleEdit = (row) => {
router.push({
path: '/acs/nodes/form',
query: { id: row.server_id, type: row.server_type },
state: { params: row }
})
}
// 删除服务器
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除服务器"${row.name}"吗?`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'error',
draggable: true,
distinguishCancelAndClose: true,
closeOnClickModal: false
}
).then(async () => {
try {
const res = await deleteServer({ server_id: row.server_id })
if (res && res.data && res.data.code === 200) {
ElNotification({
title: '删除成功',
message: `服务器"${row.name}"已被成功删除`,
type: 'success',
duration: 3000
})
fetchData()
} else {
ElMessage.error('删除失败!该数据关联了其他数据!')
}
} catch (error) {
console.error('删除服务器失败:', error)
ElMessage.error('删除服务器失败')
}
}).catch(() => {})
}
// 切换服务器在购物车中的显示状态
const handleVisibilityChange = async (row) => {
try {
const formData = {
...row,
hide: row.hide // 表格中已经修改了这个值
}
const res = await editServer(formData)
if (res && res.data && res.data.code === 200) {
ElMessage.success(`${row.hide === 0 ? '显示' : '隐藏'}该服务器在购物车中的展示`)
} else {
ElMessage.error('操作失败,请重试')
// 恢复原值
row.hide = row.hide === 0 ? 1 : 0
}
} catch (error) {
console.error('更新服务器状态失败:', error)
ElMessage.error('操作失败,请重试')
// 恢复原值
row.hide = row.hide === 0 ? 1 : 0
}
}
// 管理服务器
const handleManage = (row) => {
router.push(`/servers/server?server_id=${row.server_id}&type=${serverType.value}`)
}
// 监听服务器类型变化
watch(serverType, () => {
pagination.currentPage = 1
fetchData()
})
// 初始加载
onMounted(async () => {
try {
await fetchData()
} catch (error) {
console.error('初始化失败:', error)
ElMessage.error('页面初始化失败')
}
})
</script>
<style scoped>
.nodes-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;
}
.online-card .stat-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67C23A;
}
.offline-card .stat-icon {
background-color: rgba(144, 147, 153, 0.1);
color: #909399;
}
.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;
}
/* 状态标签 */
.status-tag {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.online {
background-color: #67C23A;
box-shadow: 0 0 4px rgba(103, 194, 58, 0.5);
}
.status-dot.offline {
background-color: #909399;
}
/* 表格样式优化 */
: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>