1094 lines
29 KiB
Vue
1094 lines
29 KiB
Vue
<template>
|
|
<div class="nodes-container">
|
|
<!-- 页面标题和操作按钮 -->
|
|
<div class="page-header">
|
|
<div class="left">
|
|
<h2 class="title">服务器管理</h2>
|
|
<el-tag type="info" effect="plain" class="count-tag">共 {{ serverStats.total }} 台服务器</el-tag>
|
|
</div>
|
|
<div class="actions">
|
|
<el-radio-group v-model="serverType" size="large" 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-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">{{ 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>
|
|
|
|
<!-- 搜索和筛选 -->
|
|
<div class="filter-section">
|
|
<el-input
|
|
v-model="filterForm.name"
|
|
placeholder="搜索服务器名称"
|
|
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>
|
|
|
|
<!-- 服务器列表 -->
|
|
<div class="table-container">
|
|
<el-table
|
|
v-loading="loading"
|
|
:data="serverData"
|
|
border
|
|
stripe
|
|
style="width: 100%"
|
|
table-layout="auto"
|
|
class="server-table"
|
|
>
|
|
<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">
|
|
<div class="action-buttons">
|
|
<el-tooltip content="管理服务器" placement="top" :hide-after="1500">
|
|
<el-button
|
|
type="primary"
|
|
:icon="Menu"
|
|
circle
|
|
@click="handleManage(scope.row)"
|
|
/>
|
|
</el-tooltip>
|
|
<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' ? '添加服务器' : '编辑服务器'"
|
|
:width="dialogWidth"
|
|
destroy-on-close
|
|
:close-on-click-modal="false"
|
|
:before-close="handleDialogClose"
|
|
class="server-dialog"
|
|
>
|
|
<el-form :model="serverForm" label-width="120px" :rules="rules" ref="serverFormRef">
|
|
<el-form-item label="服务器名称" prop="name">
|
|
<el-input v-model="serverForm.name" placeholder="请输入服务器名称" />
|
|
</el-form-item>
|
|
<el-form-item label="IP地址" prop="server_ip">
|
|
<el-input v-model="serverForm.server_ip" placeholder="请输入服务器IP" />
|
|
</el-form-item>
|
|
<el-form-item label="地区" prop="location">
|
|
<el-cascader
|
|
v-model="locationArray"
|
|
:options="regionsBuff"
|
|
:props="optionProps"
|
|
placeholder="请选择位置"
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<div class="form-section-title">服务器配置</div>
|
|
<div class="form-grid">
|
|
<el-form-item label="带宽">
|
|
<el-input v-model="serverForm.bandwidth" placeholder="带宽/mbps">
|
|
<template #append>Mbps</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
<el-form-item label="硬盘">
|
|
<el-input v-model="serverForm.disk" placeholder="硬盘大小">
|
|
<template #append>GB</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
<el-form-item label="内存">
|
|
<el-input v-model="serverForm.memory" placeholder="内存大小">
|
|
<template #append>MB</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
<el-form-item label="CPU">
|
|
<el-input v-model="serverForm.cpu" placeholder="CPU核心数">
|
|
<template #append>核</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<div class="form-section-title">服务器类型</div>
|
|
<el-form-item label="类型选项">
|
|
<el-radio-group v-model="serverForm.server_type">
|
|
<el-radio-button value="dockerContainer">容器云服务器</el-radio-button>
|
|
<el-radio-button value="hyperV">虚拟机云服务器</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<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="服务器管理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
|
|
v-model="serverForm.server_token"
|
|
placeholder="节点服务器管理员的token"
|
|
type="password"
|
|
show-password
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="展示卡片">
|
|
<el-input
|
|
v-model="serverForm.html"
|
|
type="textarea"
|
|
:rows="3"
|
|
placeholder="购买展示卡片的样式"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="控制台连接">
|
|
<el-input v-model="serverForm.console_url" placeholder="若使用https协议则必须进行反向代理,默认可不填写" />
|
|
</el-form-item>
|
|
<el-form-item label="购物车显示">
|
|
<el-switch
|
|
v-model="serverForm.hide"
|
|
:active-value="0"
|
|
:inactive-value="1"
|
|
active-text="显示"
|
|
inactive-text="隐藏"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<div class="left-actions">
|
|
<!-- <el-button @click="getFormContent" :icon="Download">粘贴配置</el-button> -->
|
|
<!-- <el-button @click="copyFormContent" :icon="Upload">复制配置</el-button> -->
|
|
</div>
|
|
<div class="right-actions">
|
|
<el-button @click="handleDialogClose">取消</el-button>
|
|
<el-button type="primary" @click="submitForm">确认</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import {
|
|
Plus, Refresh, Search, Edit, Delete, Menu,
|
|
Monitor, CircleCheck, CircleClose,
|
|
Download, Upload, Cpu
|
|
} from '@element-plus/icons-vue'
|
|
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
|
import { getServer, addServer, editServer, deleteServer, getServerStatus } from '@/utils/acs/server'
|
|
import { copyDomText } from "@/utils/hide"
|
|
|
|
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 serverFormRef = ref(null)
|
|
const serverForm = reactive({
|
|
server_id: '',
|
|
name: '',
|
|
server_ip: '',
|
|
location: '',
|
|
bandwidth: '',
|
|
disk: '',
|
|
memory: '',
|
|
cpu: '',
|
|
state: '',
|
|
auth_id: '',
|
|
server_token: '',
|
|
server_type: 'dockerContainer',
|
|
html: '',
|
|
hide: 0,
|
|
console_url: ''
|
|
})
|
|
|
|
const rules = {
|
|
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
|
|
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' }
|
|
]
|
|
}
|
|
|
|
// 处理搜索
|
|
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 = () => {
|
|
dialogType.value = 'add'
|
|
dialogVisible.value = true
|
|
|
|
// 重置表单
|
|
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 {
|
|
serverForm[key] = ''
|
|
}
|
|
})
|
|
|
|
// 确保在下一个 tick 重置表单验证状态
|
|
nextTick(() => {
|
|
if (serverFormRef.value) {
|
|
serverFormRef.value.resetFields()
|
|
}
|
|
})
|
|
}
|
|
|
|
// 编辑服务器
|
|
const handleEdit = (row) => {
|
|
dialogType.value = 'edit'
|
|
dialogVisible.value = true
|
|
|
|
// 填充表单
|
|
Object.keys(serverForm).forEach(key => {
|
|
if (key in row) {
|
|
serverForm[key] = row[key]
|
|
}
|
|
})
|
|
|
|
nextTick(() => {
|
|
if (serverFormRef.value) {
|
|
serverFormRef.value.clearValidate()
|
|
}
|
|
})
|
|
}
|
|
|
|
// 删除服务器
|
|
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}`)
|
|
}
|
|
|
|
// 关闭对话框
|
|
const handleDialogClose = () => {
|
|
dialogVisible.value = false
|
|
if (serverFormRef.value) {
|
|
serverFormRef.value.resetFields()
|
|
}
|
|
}
|
|
|
|
// 提交表单
|
|
const submitForm = async () => {
|
|
if (!serverFormRef.value) return
|
|
|
|
await serverFormRef.value.validate(async (valid) => {
|
|
if (valid) {
|
|
try {
|
|
// 确保数值类型字段为数字
|
|
const formData = { ...serverForm }
|
|
|
|
// 转换可能的数字字段
|
|
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])
|
|
}
|
|
})
|
|
|
|
let res
|
|
if (dialogType.value === 'add') {
|
|
res = await addServer(formData)
|
|
} else {
|
|
res = await editServer(formData)
|
|
}
|
|
|
|
if (res && res.data && res.data.code === 200) {
|
|
ElNotification({
|
|
title: dialogType.value === 'add' ? '添加成功' : '更新成功',
|
|
message: `服务器"${formData.name}"已${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('提交失败')
|
|
}
|
|
} else {
|
|
ElMessage.warning('请完善表单信息')
|
|
}
|
|
})
|
|
}
|
|
|
|
// 地区数据
|
|
const regionsBuff = ref([])
|
|
|
|
// 定义选项配置
|
|
const optionProps = {
|
|
label: 'label',
|
|
value: 'value',
|
|
children: 'children',
|
|
checkStrictly: false,
|
|
emitPath: true
|
|
}
|
|
|
|
// 地区数据转换
|
|
const locationArray = computed({
|
|
get: () => {
|
|
if (serverForm.location) {
|
|
try {
|
|
const labels = serverForm.location.split(' ')
|
|
const values = labels.map(label => findValueByLabel(label, regionsBuff.value))
|
|
return values.filter(value => value !== undefined)
|
|
} catch (error) {
|
|
console.error('解析地区数据失败:', error)
|
|
return []
|
|
}
|
|
} else {
|
|
return []
|
|
}
|
|
},
|
|
set: (newArray) => {
|
|
try {
|
|
if (Array.isArray(newArray) && newArray.length > 0) {
|
|
const labels = newArray.map(value => {
|
|
const label = findLabelByValue(value, regionsBuff.value)
|
|
return label || value
|
|
})
|
|
serverForm.location = labels.join(' ')
|
|
} else {
|
|
serverForm.location = ''
|
|
}
|
|
} catch (error) {
|
|
console.error('设置地区数据失败:', error)
|
|
serverForm.location = ''
|
|
}
|
|
}
|
|
})
|
|
|
|
const findValueByLabel = (label, options) => {
|
|
for (const option of options) {
|
|
if (option.label === label) {
|
|
return option.value
|
|
}
|
|
if (option.children) {
|
|
const result = findValueByLabel(label, option.children)
|
|
if (result) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const findLabelByValue = (value, options) => {
|
|
for (const option of options) {
|
|
if (option.value === value) {
|
|
return option.label
|
|
}
|
|
if (option.children) {
|
|
const result = findLabelByValue(value, option.children)
|
|
if (result) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
// 计算对话框宽度
|
|
const dialogWidth = computed(() => {
|
|
const width = window.innerWidth
|
|
if (width < 768) return '95%'
|
|
if (width < 992) return '80%'
|
|
return '60%'
|
|
})
|
|
|
|
// 复制表单内容
|
|
const copyFormContent = () => {
|
|
const formContent = JSON.parse(JSON.stringify(serverForm))
|
|
copyDomText(JSON.stringify(formContent, null, 2))
|
|
ElMessage.success('已复制服务器配置到剪贴板')
|
|
}
|
|
|
|
// 粘贴表单内容
|
|
const getFormContent = async () => {
|
|
try {
|
|
const text = await navigator.clipboard.readText()
|
|
const data = JSON.parse(text)
|
|
Object.keys(serverForm).forEach(key => {
|
|
if (key in data) {
|
|
serverForm[key] = data[key]
|
|
}
|
|
})
|
|
ElMessage.success('已从剪贴板导入服务器配置')
|
|
} catch (err) {
|
|
console.error('读取剪贴板失败:', err)
|
|
ElMessage.error('无法读取剪贴板内容,请检查获取到的格式')
|
|
}
|
|
}
|
|
|
|
// 监听窗口大小变化
|
|
const updateDialogWidth = () => {
|
|
window.addEventListener('resize', () => {
|
|
dialogWidth.value = computed(() => {
|
|
const width = window.innerWidth
|
|
if (width < 768) return '95%'
|
|
if (width < 992) return '80%'
|
|
return '60%'
|
|
}).value
|
|
})
|
|
}
|
|
|
|
// 监听服务器类型变化
|
|
watch(serverType, async (newVal) => {
|
|
if (newVal) {
|
|
try {
|
|
await fetchData()
|
|
} catch (error) {
|
|
console.error('获取数据失败:', error)
|
|
}
|
|
}
|
|
})
|
|
|
|
// 初始加载
|
|
onMounted(async () => {
|
|
try {
|
|
// 加载地区数据
|
|
// 实际项目中应该从API获取或使用静态JSON文件
|
|
regionsBuff.value = [
|
|
{
|
|
value: 'china',
|
|
label: '中国',
|
|
children: [
|
|
{
|
|
value: 'southwest',
|
|
label: '西南',
|
|
children: [
|
|
{ value: 'sichuan', label: '四川' },
|
|
{ value: 'chongqing', label: '重庆' },
|
|
{ value: 'yunnan', label: '云南' },
|
|
{ value: 'guizhou', label: '贵州' },
|
|
{ value: 'xizang', label: '西藏' }
|
|
]
|
|
},
|
|
{
|
|
value: 'northeast',
|
|
label: '东北',
|
|
children: [
|
|
{ value: 'liaoning', label: '辽宁' },
|
|
{ value: 'jilin', label: '吉林' },
|
|
{ value: 'heilongjiang', label: '黑龙江' }
|
|
]
|
|
},
|
|
{
|
|
value: 'northwest',
|
|
label: '西北',
|
|
children: [
|
|
{ value: 'shaanxi', label: '陕西' },
|
|
{ value: 'gansu', label: '甘肃' },
|
|
{ value: 'qinghai', label: '青海' },
|
|
{ value: 'ningxia', label: '宁夏' },
|
|
{ value: 'xinjiang', label: '新疆' }
|
|
]
|
|
},
|
|
{
|
|
value: 'central',
|
|
label: '华中',
|
|
children: [
|
|
{ value: 'henan', label: '河南' },
|
|
{ value: 'hubei', label: '湖北' },
|
|
{ value: 'hunan', label: '湖南' }
|
|
]
|
|
},
|
|
{
|
|
value: 'northchina',
|
|
label: '华北',
|
|
children: [
|
|
{ value: 'tianjin', label: '天津' },
|
|
{ value: 'hebei', label: '河北' },
|
|
{ value: 'shanxi', label: '山西' },
|
|
{ value: 'neimenggu', label: '内蒙古' }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
value: 'foreign',
|
|
label: '国外',
|
|
children: [
|
|
{ value: 'asia', label: '亚洲' },
|
|
{ value: 'europe', label: '欧洲' },
|
|
{ value: 'america', label: '美洲' },
|
|
{ value: 'africa', label: '非洲' },
|
|
{ value: 'oceania', label: '大洋洲' }
|
|
]
|
|
}
|
|
]
|
|
|
|
// 设置对话框宽度响应式
|
|
updateDialogWidth()
|
|
|
|
// 获取服务器列表
|
|
await fetchData()
|
|
} catch (error) {
|
|
console.error('初始化失败:', error)
|
|
ElMessage.error('页面初始化失败')
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.nodes-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;
|
|
}
|
|
|
|
.server-type-selector {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
/* 服务器统计卡片 */
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.server-table {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* 服务器状态标签 */
|
|
.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 8px rgba(103, 194, 58, 0.6);
|
|
}
|
|
|
|
.status-dot.offline {
|
|
background-color: #909399;
|
|
}
|
|
|
|
/* 操作按钮 */
|
|
.action-buttons {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* 分页 */
|
|
.pagination-container {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
/* 表单样式 */
|
|
.server-dialog :deep(.el-form-item__label) {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.form-section-title {
|
|
font-weight: 600;
|
|
margin: 16px 0 8px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px dashed #ebeef5;
|
|
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);
|
|
gap: 16px;
|
|
}
|
|
|
|
/* 对话框底部 */
|
|
.dialog-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
}
|
|
|
|
.left-actions, .right-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media screen and (max-width: 1200px) {
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@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> |