Files
ApiServer-Web-admin_dashboa…/src/views/acs/nodes/server.vue
T
2025-07-15 18:02:29 +08:00

1373 lines
39 KiB
Vue

<template>
<div class="server-container">
<!-- 页面标题和状态栏 -->
<div class="page-header">
<div class="left">
<h2 class="title">服务器详情</h2>
<el-tag
:type="serverMessage.state == 0 ? 'danger' : 'success'"
effect="dark"
class="status-tag"
>
<span class="status-dot" :class="serverMessage.state == 0 ? 'offline' : 'online'"></span>
{{ serverMessage.state == 0 ? "离线" : "在线" }}
</el-tag>
</div>
<div class="actions">
<el-button type="primary" @click="initData" :icon="Refresh">
刷新数据
</el-button>
</div>
</div>
<!-- 服务器信息卡片 -->
<div class="server-info">
<div class="info-card main-info">
<div class="card-title">
<el-icon><Monitor /></el-icon>
<span>基本信息</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">服务器名称</div>
<div class="info-value highlight">{{ serverMessage.name || '未设置' }}</div>
</div>
<div class="info-item">
<div class="info-label">服务器ID</div>
<div class="info-value">{{ serverMessage.server_id || '未知' }}</div>
</div>
<div class="info-item">
<div class="info-label">IP地址</div>
<div class="info-value">{{ serverMessage.server_ip || '未设置' }}</div>
</div>
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">{{ serverMessage.created_at || '未知' }}</div>
</div>
</div>
</div>
<div class="info-card resource-info">
<div class="card-title">
<el-icon><CpuIcon /></el-icon>
<span>资源配置</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">CPU</div>
<div class="info-value">{{ serverMessage.cpu || '0' }} 核心</div>
</div>
<div class="info-item">
<div class="info-label">内存</div>
<div class="info-value">{{ serverMessage.memory || '0' }} GB</div>
</div>
<div class="info-item">
<div class="info-label">硬盘</div>
<div class="info-value">{{ serverMessage.disk || '0' }} GB</div>
</div>
<div class="info-item">
<div class="info-label">带宽</div>
<div class="info-value">{{ serverMessage.bandwidth || '0' }} Mbps</div>
</div>
</div>
</div>
<div class="info-card location-info">
<div class="card-title">
<el-icon><Location /></el-icon>
<span>位置信息</span>
</div>
<div class="info-content">
<div class="info-item location">
<div class="info-label">地区</div>
<div class="info-value">{{ serverMessage.location || '未设置' }}</div>
</div>
</div>
</div>
</div>
<!-- 图表 -->
<serverChart v-if="TypeData" :Type="TypeData" class="chart-section" />
<!-- 主要内容区域 -->
<div class="content-wrapper">
<el-tabs type="border-card" class="main-tabs">
<!-- 实例规格列表 -->
<el-tab-pane label="实例规格列表">
<div class="tab-header">
<h3 class="tab-title">实例规格管理</h3>
<el-button
type="primary"
@click="show_spec(); centerDialogVisible = true; addOrChange = true;"
:icon="Plus"
>
添加实例规格
</el-button>
</div>
<el-table
v-loading="specLoading"
:data="spec_list"
border
stripe
style="width: 100%"
table-layout="auto"
class="data-table"
>
<el-table-column prop="plan_id" label="规格ID" width="80" />
<el-table-column prop="name" label="规格名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="memory" label="内存" width="100">
<template #default="scope">
<div class="resource-value">{{ scope.row.memory }} <span class="unit">MB</span></div>
</template>
</el-table-column>
<el-table-column prop="disk" label="硬盘" width="100">
<template #default="scope">
<div class="resource-value">{{ scope.row.disk }} <span class="unit">GB</span></div>
</template>
</el-table-column>
<el-table-column prop="cpu" label="CPU" width="100">
<template #default="scope">
<div class="resource-value">{{ scope.row.cpu }} <span class="unit">{{ TypeData === 'dockerContainer' ? '毫核' : '核' }}</span></div>
</template>
</el-table-column>
<el-table-column v-if="TypeData === 'dockerContainer'" prop="bandwidth_tx" label="上行带宽" width="120">
<template #default="scope">
<div class="resource-value">{{ scope.row.bandwidth_tx }} <span class="unit">Mbps</span></div>
</template>
</el-table-column>
<el-table-column v-if="TypeData === 'dockerContainer'" prop="bandwidth_rx" label="下行带宽" width="120">
<template #default="scope">
<div class="resource-value">{{ scope.row.bandwidth_rx }} <span class="unit">Mbps</span></div>
</template>
</el-table-column>
<el-table-column prop="price" label="价格" width="100">
<template #default="scope">
<div class="price-tag">¥ {{ scope.row.price }}</div>
</template>
</el-table-column>
<el-table-column prop="number" label="剩余数量" width="100">
<template #default="scope">
<el-tag :type="scope.row.number > 10 ? 'success' : scope.row.number > 0 ? 'warning' : 'danger'" effect="plain">
{{ scope.row.number }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<div class="table-actions">
<el-tooltip content="编辑" placement="top" :hide-after="1500">
<el-button
type="primary"
circle
:icon="Edit"
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
/>
</el-tooltip>
<el-tooltip content="删除" placement="top" :hide-after="1500">
<el-button
type="danger"
circle
:icon="Delete"
@click="deleteSpec(scope.row.plan_id)"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<el-empty v-if="spec_list.length === 0" description="暂无实例规格数据" />
</el-tab-pane>
<!-- 容器列表 -->
<el-tab-pane v-if="TypeData == 'dockerContainer'" label="容器列表">
<div class="tab-header">
<h3 class="tab-title">容器管理</h3>
<el-input
v-model="containerBox.key"
placeholder="搜索容器..."
class="search-input"
:prefix-icon="Search"
clearable
/>
</div>
<el-table
:data="user_servers"
stripe
border
style="width: 100%"
class="data-table"
>
<el-table-column label="ID" prop="container_id" width="80" />
<el-table-column label="价格" prop="pay" width="100">
<template #default="scope">
<div class="price-tag">¥ {{ scope.row.pay }}</div>
</template>
</el-table-column>
<el-table-column label="购买时间" min-width="160">
<template #default="scope">
<div class="time-info">{{ scope.row.created_at }}</div>
</template>
</el-table-column>
<el-table-column label="到期时间" min-width="160">
<template #default="scope">
<div class="time-info">{{ scope.row.become_time }}</div>
</template>
</el-table-column>
<el-table-column label="规格" prop="plan_id" width="80" />
<el-table-column label="用户ID" prop="user_id" width="80" />
<el-table-column label="状态" width="100" align="center">
<template #default="scope">
<el-tag
:type="scope.row.container_state == 0 || scope.row.container_state == 1
? 'info'
: scope.row.container_state == 2
? 'success'
: 'danger'"
effect="light"
>
{{
scope.row.container_state == 0
? "未支付"
: scope.row.container_state == 1
? "未开通"
: scope.row.container_state == 2
? "开机"
: "关机"
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
:icon="Setting"
@click="$router.push('/servers/containers?container_id=' + scope.row.container_id)"
>
管理
</el-button>
</template>
</el-table-column>
</el-table>
<!--分页-->
<div class="pagination-container">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page="containerBox.page"
:page-size="containerBox.count"
:page-sizes="[5, 10, 20, 50]"
@update:current-page="handleCurrentPageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</el-tab-pane>
<!-- 虚拟机列表 -->
<el-tab-pane v-if="TypeData == 'hyperV'" label="虚拟机列表" :lazy="true">
<VmList :ID="serverMessage.server_id" />
</el-tab-pane>
<!-- 浮动IP池 -->
<el-tab-pane v-if="TypeData == 'dockerContainer'" label="浮动IP池" :lazy="true">
<div class="tab-header">
<h3 class="tab-title">浮动IP管理</h3>
<div class="action-btns">
<el-button type="primary" @click="floatVisible = true" :icon="Plus">
添加浮动IP
</el-button>
<el-button type="primary" @click="addMore = true" :icon="Plus">
批量添加浮动IP
</el-button>
</div>
</div>
<el-table
:data="floatList"
style="width: 100%"
border
stripe
class="data-table"
>
<el-table-column label="ID" prop="id" width="80" />
<el-table-column label="创建时间" min-width="160">
<template #default="scope">
<div class="time-info">{{ scope.row.created_at }}</div>
</template>
</el-table-column>
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
<el-table-column label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.is_used ? 'success' : 'info'" effect="light">
{{ scope.row.is_used ? "已绑定" : "未绑定" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-tooltip content="删除IP" placement="top" :hide-after="1500">
<el-button
type="danger"
circle
:icon="Delete"
@click="delFloating(scope.row.id)"
/>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
</el-tab-pane>
</el-tabs>
</div>
<!-- 添加/编辑实例规格对话框 -->
<el-dialog
v-model="centerDialogVisible"
:title="addOrChange ? '添加实例规格' : '编辑实例规格'"
width="650px"
top="5vh"
destroy-on-close
class="spec-dialog"
>
<el-form :model="spec_form" label-width="140px">
<div class="form-section">
<div class="section-title">基本信息</div>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="规格名称">
<el-input v-model="spec_form.name" placeholder="请输入实例规格名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="spec_form.description" placeholder="对实例规格的介绍" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section">
<div class="section-title">资源配置</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="内存">
<el-input v-model="spec_form.memory" placeholder="内存限制" type="number">
<template #append>MB</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="硬盘">
<el-input v-model="spec_form.disk" placeholder="硬盘限制" type="number">
<template #append>GB</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TypeData == 'dockerContainer' ? 'CPU(毫核)' : 'CPU(核)'">
<el-input
v-model="spec_form.cpu"
:placeholder="TypeData == 'dockerContainer' ? 'CPU 限制 (1核=1000毫核)' : 'CPU 限制'"
type="number"
>
<template #append>{{ TypeData == 'dockerContainer' ? '毫核' : '核' }}</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="库存数量">
<el-input v-model="spec_form.number" placeholder="库存数量" type="number">
<template #append></template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section" v-if="TypeData == 'dockerContainer'">
<div class="section-title">网络配置</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="上行带宽">
<el-input v-model="spec_form.bandwidth_tx" placeholder="上行带宽" type="number">
<template #append>Mbps</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="下行带宽">
<el-input v-model="spec_form.bandwidth_rx" placeholder="下行带宽" type="number">
<template #append>Mbps</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="上行流量阈值">
<el-input v-model="spec_form.threshold_tx" placeholder="上行流量报警阈值" type="number">
<template #append>Mbps</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="下行流量阈值">
<el-input v-model="spec_form.threshold_rx" placeholder="下行流量报警阈值" type="number">
<template #append>Mbps</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section" v-if="TypeData == 'hyperV'">
<div class="section-title">虚拟机特性</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="允许开放端口数">
<el-input v-model="spec_form.port_num" placeholder="默认允许开放端口数" type="number">
<template #append></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="允许快照数">
<el-input v-model="spec_form.snapshot_num" placeholder="默认允许快照数" type="number">
<template #append></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="读写限制">
<el-input v-model="spec_form.min_iops" placeholder="读写限制(1iops=8kb/s)" type="number">
<template #append>IOPS</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="读写最大突发">
<el-input v-model="spec_form.max_iops" placeholder="读写最大突发" type="number">
<template #append>IOPS</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section">
<div class="section-title">价格与验证</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="价格">
<el-input v-model="spec_form.price" placeholder="价格" type="number">
<template #prepend>¥</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="额外硬盘价格">
<el-input v-model="spec_form.volume_price" placeholder="额外硬盘价格" type="number">
<template #prepend>¥</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否需要实名">
<el-switch
v-model="spec_form.must_real_name"
:active-value="1"
:inactive-value="0"
active-text="需要实名"
inactive-text="不需要实名"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<div class="left-actions">
<el-tooltip content="粘贴配置" placement="top">
<el-button @click="getit" :icon="Download">
粘贴
</el-button>
</el-tooltip>
<el-tooltip content="复制配置" placement="top">
<el-button @click="copyit" :icon="Upload">
复制
</el-button>
</el-tooltip>
</div>
<div class="right-actions">
<el-button @click="centerDialogVisible = false">取消</el-button>
<el-button type="primary" @click="editSpec">确认</el-button>
</div>
</div>
</template>
</el-dialog>
<!-- 添加浮动IP对话框 -->
<el-dialog
v-model="floatVisible"
title="添加浮动IP"
width="480px"
destroy-on-close
class="float-dialog"
>
<el-form label-width="120px">
<el-form-item label="浮动IP地址">
<el-input
v-model="newFloatingIp"
type="textarea"
:autosize="{ minRows: 3, maxRows: 10 }"
placeholder="每个IP一行,换行添加下一个"
/>
</el-form-item>
<div class="ip-tips">
<el-alert
title="添加提示"
type="info"
description="请确保每行一个IP,且格式正确"
:closable="false"
show-icon
/>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="floatVisible = false">取消</el-button>
<el-button type="primary" @click="ToAddFloatingIp">确认添加</el-button>
</div>
</template>
</el-dialog>
<!-- 批量添加浮动IP对话框 -->
<el-dialog
v-model="addMore"
title="批量添加浮动IP"
width="480px"
destroy-on-close
class="float-dialog"
>
<el-form label-width="140px">
<el-form-item label="起始浮动IP地址">
<div class="ip-input-group">
<el-input v-model="addr_floating_ip" placeholder="如:11.11.11" />
<span class="ip-separator">.</span>
<el-input v-model="start_ip" placeholder="起始值" class="ip-last-segment" />
</div>
</el-form-item>
<el-form-item label="结束浮动IP地址">
<div class="ip-input-group">
<el-input v-model="addr_floating_ip" placeholder="如:11.11.11" disabled />
<span class="ip-separator">.</span>
<el-input v-model="end_ip" placeholder="结束值" class="ip-last-segment" />
</div>
</el-form-item>
<div class="ip-tips">
<el-alert
title="批量添加说明"
type="info"
description="系统将添加从起始IP到结束IP的所有地址"
:closable="false"
show-icon
/>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addMore = false">取消</el-button>
<el-button type="primary" @click="ToAddMore">确认添加</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, ref, onMounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
import {
selectServer,
getServerPlan,
editServerPlan,
addServerPlan,
deleteServerPlan,
getContainer,
getFloatingIpList,
addFloatingIp,
delFloatingIp,
addFloatingIpBatch
} from "@/utils/acs/server";
import { ElMessage, ElNotification } from 'element-plus';
import { copyDomText } from "@/utils/hide";
import {
Plus,
Edit,
Delete,
Download,
Upload,
Refresh,
Monitor,
Cpu as CpuIcon,
Location,
Setting,
Search
} from '@element-plus/icons-vue';
// 组件引入
import serverChart from "./components/serverChart.vue";
import VmList from "./components/VmList.vue";
const router = useRouter();
const route = useRoute();
let serverMessage = ref({});
const specLoading = ref(false);
// 初始化数据的函数
const initData = async () => {
try {
TypeData.value = route.query.type;
// 重置数据状态
serverMessage.value = {};
spec_list.value = [];
user_servers.value = [];
floatList.value = [];
// 获取服务器信息
let res = await selectServer({ server_id: route.query.server_id });
if (res && res.data && res.data.data) {
serverMessage.value = res.data.data;
ElNotification({
title: '数据刷新成功',
message: `已成功获取服务器 ${serverMessage.value.name || '未命名'} 的数据`,
type: 'success',
duration: 3000
});
}
// 获取浮动IP列表
await getFloating();
// 获取实例规格
await GetSpecs();
// 获取所有容器
let usertype = localStorage.getItem("user_type");
if (usertype == "1") {
containerBox.server_id = route.query.server_id;
let cons = await getContainer(containerBox);
if (cons && cons.data) {
user_servers.value = cons.data.data || [];
total.value = cons.data.count || 0;
}
}
} catch (error) {
console.error("初始化数据失败:", error);
ElMessage.error("加载数据失败,请刷新页面重试");
}
};
// 监听路由参数变化
watch(
() => route.query,
(newQuery) => {
if (newQuery.server_id) {
initData();
}
},
{ deep: true }
);
onMounted(() => {
initData();
});
const TypeData = ref("");
//获取服务器实例规格
const GetSpecs = async () => {
specLoading.value = true;
try {
let plans = await getServerPlan({
server_id: route.query.server_id,
count: 30
});
console.log("服务器实例规格", plans.data.data);
spec_list.value = plans.data.data;
} catch (error) {
console.error("获取实例规格列表失败:", error);
ElMessage.error("获取实例规格列表失败");
} finally {
specLoading.value = false;
}
};
//获取浮动ip列表
const getFloating = async () => {
try {
let res = await getFloatingIpList({ server_id: route.query.server_id });
if (res && res.data) {
floatList.value = res.data.data || [];
floatTotal.value = res.data.count || 0;
}
} catch (error) {
console.error("获取浮动IP列表失败:", error);
ElMessage.error("获取浮动IP列表失败");
floatList.value = [];
floatTotal.value = 0;
}
};
const floatTotal = ref(10);
const floatList = ref([]);
const floatVisible = ref(false);
const newFloatingIp = ref("");
//新增浮动ip
const ToAddFloatingIp = async () => {
if (!newFloatingIp.value.trim()) {
ElMessage.warning('请输入至少一个IP地址');
return;
}
let arr = newFloatingIp.value.split("\n").filter(ip => ip.trim().length > 0);
if (arr.length === 0) {
ElMessage.warning('没有有效的IP地址');
return;
}
let promises = arr.map(item => {
return addFloatingIp({
server_id: route.query.server_id,
floating_ip: item.trim()
}).then(res => {
if (res.data.code !== 200) {
return Promise.reject(new Error(`${res.data.message}`));
}
return res;
});
});
try {
ElMessage.info(`正在添加 ${arr.length} 个IP地址...`);
const results = await Promise.allSettled(promises);
let successCount = 0;
let failList = [];
results.forEach((result, index) => {
if (result.status === "fulfilled") {
successCount++;
} else {
failList.push(`IP ${arr[index]}: ${result.reason.message}`);
ElMessage.error(`IP ${arr[index]} 添加失败: ${result.reason.message}`);
}
});
if (successCount > 0) {
ElNotification({
title: 'IP添加结果',
message: `${successCount}个IP添加成功,${arr.length - successCount}个失败`,
type: successCount === arr.length ? 'success' : 'warning',
duration: 5000
});
}
getFloating();
floatVisible.value = false;
newFloatingIp.value = '';
} catch (error) {
ElMessage.error("处理过程中发生未知错误");
}
};
//删除浮动ip
const delFloating = async (id) => {
try {
let res = await delFloatingIp({ id: id });
if (res.data.code == 200) {
ElMessage.success("删除成功");
getFloating();
} else {
ElMessage.error(res.data.message || "删除失败");
}
} catch (error) {
console.error("删除浮动IP失败:", error);
ElMessage.error("删除失败,请重试");
}
};
//批量添加浮动IP
const addMore = ref(false);
const addr_floating_ip = ref("");
const start_ip = ref("");
const end_ip = ref("");
const ToAddMore = async () => {
if (!addr_floating_ip.value || !start_ip.value || !end_ip.value) {
ElMessage.warning('请填写完整的IP地址信息');
return;
}
try {
const startNum = parseInt(start_ip.value);
const endNum = parseInt(end_ip.value);
if (isNaN(startNum) || isNaN(endNum)) {
ElMessage.error('IP地址的最后一段必须是数字');
return;
}
if (startNum > endNum) {
ElMessage.error('起始IP必须小于或等于结束IP');
return;
}
if (endNum - startNum > 100) {
const confirmed = await ElMessageBox.confirm(
`您将添加${endNum - startNum + 1}个IP地址,确定继续吗?`,
'批量添加确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).catch(() => false);
if (!confirmed) return;
}
ElMessage.info(`正在批量添加IP地址...`);
const res = await addFloatingIpBatch({
server_id: route.query.server_id,
start_floating_ip: `${addr_floating_ip.value}.${start_ip.value}`,
end_floating_ip: `${addr_floating_ip.value}.${end_ip.value}`
});
if (res.data.code == 200) {
ElNotification({
title: '批量添加成功',
message: `已成功添加从 ${addr_floating_ip.value}.${start_ip.value}${addr_floating_ip.value}.${end_ip.value} 的IP地址`,
type: 'success',
duration: 5000
});
addMore.value = false;
getFloating();
// 重置表单
start_ip.value = '';
end_ip.value = '';
} else {
ElMessage.error(res.data.message || "批量添加失败");
}
} catch (error) {
console.error("批量添加IP失败:", error);
ElMessage.error("操作失败,请重试");
}
};
//容器相关数据
const containerBox = reactive({
server_id: "",
page: 1,
count: 10,
key: "",
user_id: ""
});
//修改新增实例规格的弹窗开关变量
const centerDialogVisible = ref(false);
//修改或新增实例规格的判断
const addOrChange = ref(false);
const editSpec = async () => {
if (!spec_form.name || !spec_form.memory || !spec_form.disk || !spec_form.cpu || !spec_form.price) {
ElMessage.warning('请填写必要的规格信息');
return;
}
try {
if (addOrChange.value) {
const res = await addServerPlan({
...spec_form,
server_id: route.query.server_id
});
if (res.data.code == 200) {
ElNotification({
title: '添加成功',
message: `已成功添加规格 "${spec_form.name}"`,
type: 'success',
duration: 3000
});
centerDialogVisible.value = false;
} else {
ElMessage.error(res.data.message || "添加失败");
return;
}
} else {
const res = await editServerPlan(spec_form);
if (res.data.code == 200) {
ElNotification({
title: '修改成功',
message: `已成功修改规格 "${spec_form.name}"`,
type: 'success',
duration: 3000
});
centerDialogVisible.value = false;
} else {
ElMessage.error(res.data.message || "修改失败");
return;
}
}
//更新服务器实例规格
await GetSpecs();
} catch (error) {
console.error("操作实例规格失败:", error);
ElMessage.error("操作失败,请重试");
}
};
//删除实例规格
const deleteSpec = async (id) => {
try {
const confirmed = await ElMessageBox.confirm(
'删除实例规格将影响使用该规格的用户,确定要删除吗?',
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'error'
}
).catch(() => false);
if (!confirmed) return;
const res = await deleteServerPlan({
plan_id: id,
server_type: TypeData.value
});
if (res.data.code == 200) {
ElMessage.success("删除成功!");
//更新服务器实例规格
await GetSpecs();
} else {
ElMessage.error(res.data.message || "删除失败");
}
} catch (error) {
console.error("删除实例规格失败:", error);
ElMessage.error("删除失败,请重试");
}
};
const spec_list = ref([]);
//容器列表
const user_servers = ref([]);
//容器分页
const total = ref(10);
const handleCurrentPageChange = async newPage => {
try {
containerBox.page = newPage;
let cons = await getContainer(containerBox);
if (cons && cons.data) {
user_servers.value = cons.data.data || [];
total.value = cons.data.count || 0;
}
} catch (error) {
console.error("获取容器列表失败:", error);
ElMessage.error("获取容器列表失败");
}
};
const handlePageSizeChange = async newSize => {
try {
containerBox.count = newSize;
containerBox.page = 1; // 切换每页条数时重置为第一页
let cons = await getContainer(containerBox);
if (cons && cons.data) {
user_servers.value = cons.data.data || [];
total.value = cons.data.count || 0;
}
} catch (error) {
console.error("获取容器列表失败:", error);
ElMessage.error("获取容器列表失败");
}
};
const spec_form = reactive({
plan_id: "",
name: "",
memory: "",
disk: "",
cpu: "",
price: "",
bandwidth_rx: "",
bandwidth_tx: "",
threshold_rx: "",
threshold_tx: "",
port_num: "",
snapshot_num: "",
number: "",
volume_price: "",
min_iops: "",
max_iops: "",
description: "",
must_real_name: 0
});
function show_spec(data = null) {
if (!data) {
// 当data未传值,视为新增实例规格
// 遍历form对象的每个键值对
Object.keys(spec_form).forEach(key => {
spec_form[key] = key === 'must_real_name' ? 0 : '';
});
// 设置默认类型
spec_form.server_type = TypeData.value;
} else {
// data传值,视为修改实例规格
// 遍历data对象的每个键值对
Object.keys(data).forEach(key => {
if (key in spec_form) {
spec_form[key] = data[key];
}
});
}
}
//复制内容
const copytext = ref({});
const copyit = async () => {
try {
copytext.value = JSON.parse(JSON.stringify(spec_form));
copyDomText(JSON.stringify(copytext.value, null, 2));
ElMessage.success("已复制配置到剪贴板");
} catch (error) {
console.error("复制失败:", error);
ElMessage.error("复制失败");
}
};
const getit = async () => {
try {
let text = await navigator.clipboard.readText();
let objet = JSON.parse(text);
// 移除plan_id,避免覆盖原ID
if (addOrChange.value) {
Reflect.deleteProperty(objet, "plan_id");
}
Object.keys(spec_form).forEach(key => {
if (key in objet) {
spec_form[key] = objet[key];
}
});
ElMessage.success("配置已从剪贴板导入");
} catch (err) {
console.error("读取剪贴板失败:", err);
ElMessage.error("无法读取剪贴板内容,请检查格式是否正确");
}
};
// 导入其他需要的组件
import { ElMessageBox } from 'element-plus';
</script>
<style scoped>
.server-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 120px);
}
/* 页面标题区域 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.status-tag {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.online {
background-color: #67C23A;
box-shadow: 0 0 6px rgba(103, 194, 58, 0.8);
}
.status-dot.offline {
background-color: #F56C6C;
box-shadow: 0 0 6px rgba(245, 108, 108, 0.8);
}
/* 服务器信息卡片 */
.server-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.info-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 0;
overflow: hidden;
transition: all 0.3s;
border: 1px solid #ebeef5;
}
.info-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
}
.card-title {
background-color: #f5f7fa;
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
font-size: 16px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.card-title .el-icon {
font-size: 18px;
color: #409EFF;
}
.info-content {
padding: 16px;
}
.info-item {
margin-bottom: 12px;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.info-value {
font-size: 15px;
color: #303133;
word-break: break-all;
}
.info-value.highlight {
font-weight: 600;
font-size: 16px;
color: #409EFF;
}
/* 主要内容区域 */
.chart-section {
margin-bottom: 24px;
}
.content-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
}
.tab-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.tab-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.search-input {
width: 240px;
}
.action-btns {
display: flex;
gap: 8px;
}
/* 表格样式 */
.data-table {
margin-bottom: 16px;
}
.table-actions {
display: flex;
justify-content: center;
gap: 8px;
}
.resource-value {
white-space: nowrap;
}
.unit {
color: #909399;
font-size: 12px;
margin-left: 2px;
}
.price-tag {
font-weight: 600;
color: #F56C6C;
}
.time-info {
font-size: 13px;
color: #606266;
}
/* 分页样式 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 对话框样式 */
.form-section {
margin-bottom: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.left-actions, .right-actions {
display: flex;
gap: 8px;
}
/* IP对话框样式 */
.ip-input-group {
display: flex;
align-items: center;
}
.ip-separator {
margin: 0 8px;
font-weight: bold;
}
.ip-last-segment {
width: 80px !important;
flex-shrink: 0;
}
.ip-tips {
margin-top: 16px;
}
/* 响应式设计 */
@media screen and (max-width: 1200px) {
.server-info {
grid-template-columns: repeat(2, 1fr);
}
.info-card.location-info {
grid-column: span 2;
}
}
@media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.server-info {
grid-template-columns: 1fr;
}
.info-card.location-info {
grid-column: auto;
}
.tab-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.search-input {
width: 100%;
}
.action-btns {
width: 100%;
justify-content: space-between;
}
}
</style>