1373 lines
39 KiB
Vue
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> |