3198 lines
99 KiB
Vue
3198 lines
99 KiB
Vue
<template>
|
||
<div class="server-container">
|
||
<!-- 页面标题和状态栏 -->
|
||
<div class="page-header">
|
||
<div class="left">
|
||
<el-button
|
||
type="text"
|
||
@click="goBack"
|
||
:icon="ArrowLeft"
|
||
class="back-btn"
|
||
>
|
||
返回
|
||
</el-button>
|
||
<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" v-loading="loading">
|
||
<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>
|
||
|
||
<!-- 硬件和流量信息卡片 -->
|
||
<div class="server-detail-info" v-loading="loading">
|
||
<div class="info-card hardware-info">
|
||
<div class="card-title">
|
||
<el-icon><Monitor /></el-icon>
|
||
<span>硬件信息</span>
|
||
<span v-if="diskInfo && Array.isArray(diskInfo) && diskInfo.length > 0" class="device-count">
|
||
({{ diskInfo.length }}个设备)
|
||
</span>
|
||
</div>
|
||
|
||
<div class="info-content">
|
||
<!-- 显示所有磁盘设备信息 -->
|
||
<template v-if="diskInfo && Array.isArray(diskInfo) && diskInfo.length > 0">
|
||
<!-- 总览信息 -->
|
||
<div class="hardware-summary">
|
||
<div class="summary-item">
|
||
<div class="info-label">总容量</div>
|
||
<div class="info-value highlight">{{ getTotalCapacity() }} GB</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="info-label">已使用</div>
|
||
<div class="info-value">{{ getTotalUsed() }} GB</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="info-label">平均使用率</div>
|
||
<div class="info-value" :class="getUsageClass(getAverageUsage())">{{ getAverageUsage() }}%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备详情 -->
|
||
<div class="hardware-devices">
|
||
<div v-for="(disk, index) in diskInfo" :key="index" class="device-item">
|
||
<div class="device-header">
|
||
<div class="device-title">
|
||
<el-icon><Files /></el-icon>
|
||
{{ disk.device || `存储设备 ${index + 1}` }}
|
||
</div>
|
||
<el-tag
|
||
:type="getDeviceStatusType(disk.percent)"
|
||
size="small"
|
||
>
|
||
{{ getDeviceStatusText(disk.percent) }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<div class="device-details">
|
||
<div class="detail-row">
|
||
<div class="info-item">
|
||
<div class="info-label">总容量</div>
|
||
<div class="info-value">{{ formatSize(disk.total_gb) }} GB</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">已使用</div>
|
||
<div class="info-value">{{ formatSize(disk.used_gb) }} GB</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">可用空间</div>
|
||
<div class="info-value">{{ formatSize(disk.free_gb) }} GB</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">使用率</div>
|
||
<div class="info-value" :class="getUsageClass(disk.percent)">
|
||
{{ disk.percent }}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 使用率进度条 -->
|
||
<div class="usage-progress">
|
||
<el-progress
|
||
:percentage="disk.percent"
|
||
:color="getUsageColor(disk.percent)"
|
||
:stroke-width="6"
|
||
:show-text="false"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 数据异常时的显示 -->
|
||
<template v-else>
|
||
<div class="info-item">
|
||
<div class="info-label">硬件信息</div>
|
||
<div class="info-value">{{ typeof diskInfo === 'string' ? diskInfo : '暂无数据' }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-card real-hardware-info">
|
||
<div class="card-title">
|
||
<el-icon><CpuIcon /></el-icon>
|
||
<span>实际硬件划分</span>
|
||
</div>
|
||
<div class="info-content">
|
||
<template v-if="realDiskInfo && typeof realDiskInfo === 'object'">
|
||
<!-- 主要的磁盘划分信息 -->
|
||
<div class="info-item ">
|
||
<div class="info-label">已分配磁盘空间</div>
|
||
<div class="info-value highlight">
|
||
{{ formatRealDiskValue(realDiskInfo.allocated_disk) }} {{ realDiskInfo.unit || 'GB' }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 如果有其他硬件信息也显示 -->
|
||
<template v-if="realDiskInfo.allocated_cpu && realDiskInfo.allocated_cpu !== '暂无数据'">
|
||
<div class="info-item">
|
||
<div class="info-label">已分配CPU</div>
|
||
<div class="info-value">{{ realDiskInfo.allocated_cpu }} 核</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-if="realDiskInfo.allocated_memory && realDiskInfo.allocated_memory !== '暂无数据'">
|
||
<div class="info-item">
|
||
<div class="info-label">已分配内存</div>
|
||
<div class="info-value">{{ realDiskInfo.allocated_memory }} GB</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-if="realDiskInfo.remaining_resources && realDiskInfo.remaining_resources !== '暂无数据'">
|
||
<div class="info-item">
|
||
<div class="info-label">剩余资源</div>
|
||
<div class="info-value">{{ realDiskInfo.remaining_resources }}%</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
|
||
<!-- 数据异常时的显示 -->
|
||
<template v-else>
|
||
<div class="info-item">
|
||
<div class="info-label">硬件划分信息</div>
|
||
<div class="info-value">{{ typeof realDiskInfo === 'string' ? realDiskInfo : '暂无数据' }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-card traffic-info">
|
||
<div class="card-title">
|
||
<el-icon><Monitor /></el-icon>
|
||
<span>流量信息</span>
|
||
</div>
|
||
<div class="info-content">
|
||
<template v-if="trafficInfo && typeof trafficInfo === 'object'">
|
||
<!-- 容器服务器:实时网络速率 -->
|
||
<template v-if="TypeData === 'dockerContainer'">
|
||
<div class="traffic-section">
|
||
<div class="section-title">
|
||
实时网络速率
|
||
<el-tag type="info" size="small" style="margin-left: 8px;">实时监控</el-tag>
|
||
</div>
|
||
<div class="traffic-speed-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">下行速率</div>
|
||
<div class="info-value highlight">
|
||
{{ trafficInfo.rx_speed_display || trafficInfo.download_traffic || '0 KB/s' }}
|
||
</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">上行速率</div>
|
||
<div class="info-value highlight">
|
||
{{ trafficInfo.tx_speed_display || trafficInfo.upload_traffic || '0 KB/s' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="traffic-section">
|
||
<div class="section-title">统计说明</div>
|
||
<div class="traffic-note">
|
||
<el-alert
|
||
title="容器服务器说明"
|
||
type="info"
|
||
description="容器服务器提供实时流量速率监控,不支持历史累计流量统计"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 虚拟机服务器:实时速率 + 累计流量 -->
|
||
<template v-else>
|
||
<div class="traffic-section">
|
||
<div class="section-title">实时网络速率</div>
|
||
<div class="traffic-speed-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">下行速率</div>
|
||
<div class="info-value highlight">
|
||
{{ typeof trafficInfo.rx_speed === 'number' ? formatNetworkSpeed(trafficInfo.rx_speed) : trafficInfo.rx_speed }}
|
||
</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">上行速率</div>
|
||
<div class="info-value highlight">
|
||
{{ typeof trafficInfo.tx_speed === 'number' ? formatNetworkSpeed(trafficInfo.tx_speed) : trafficInfo.tx_speed }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 累计流量统计 -->
|
||
<div class="traffic-section">
|
||
<div class="section-title">累计流量统计</div>
|
||
<div class="traffic-total-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">下行总流量</div>
|
||
<div class="info-value">
|
||
{{ typeof trafficInfo.rx_total === 'number' ? formatTrafficBytes(trafficInfo.rx_total) : trafficInfo.rx_total }}
|
||
</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">上行总流量</div>
|
||
<div class="info-value">
|
||
{{ typeof trafficInfo.tx_total === 'number' ? formatTrafficBytes(trafficInfo.tx_total) : trafficInfo.tx_total }}
|
||
</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">总计流量</div>
|
||
<div class="info-value highlight">
|
||
{{ typeof trafficInfo.rx_total === 'number' && typeof trafficInfo.tx_total === 'number' ?
|
||
formatTrafficBytes(trafficInfo.rx_total + trafficInfo.tx_total) : '暂无数据' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
|
||
<!-- 数据异常时的显示 -->
|
||
<template v-else>
|
||
<div class="info-item">
|
||
<div class="info-label">流量信息</div>
|
||
<div class="info-value">{{ typeof trafficInfo === 'string' ? trafficInfo : '暂无数据' }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-card total-traffic-info" v-if="!isTotalTrafficError">
|
||
<div class="card-title">
|
||
<el-icon><DataAnalysis /></el-icon>
|
||
<span>历史总流量统计</span>
|
||
<el-tag type="success" size="small" style="margin-left: 8px;">已加载</el-tag>
|
||
</div>
|
||
<div class="info-content">
|
||
<!-- 总体统计 -->
|
||
<div class="traffic-section">
|
||
<div class="section-title">总体统计</div>
|
||
<div class="traffic-total-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">历史总流量</div>
|
||
<div class="info-value highlight">{{ totalTrafficInfo.total_traffic || '0.00 GB' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">平均日流量</div>
|
||
<div class="info-value">{{ totalTrafficInfo.avg_daily_traffic || '0.00 GB' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">流量趋势</div>
|
||
<div class="info-value">{{ totalTrafficInfo.traffic_trend || '稳定' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上下行分布 -->
|
||
<div class="traffic-section">
|
||
<div class="section-title">上下行流量分布</div>
|
||
<div class="traffic-distribution">
|
||
<div class="info-item">
|
||
<div class="info-label">下行总流量</div>
|
||
<div class="info-value">{{ totalTrafficInfo.download_traffic || '0.00 GB' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">上行总流量</div>
|
||
<div class="info-value">{{ totalTrafficInfo.upload_traffic || '0.00 GB' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">峰值单向流量</div>
|
||
<div class="info-value">{{ totalTrafficInfo.peak_traffic || '0.00 GB' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 原始数据显示(调试用,可选) -->
|
||
<div class="traffic-section" v-if="totalTrafficInfo.rx_bytes && totalTrafficInfo.tx_bytes">
|
||
<div class="section-title">原始数据 ({{ totalTrafficInfo.unit }})</div>
|
||
<div class="raw-data">
|
||
<div class="info-item">
|
||
<div class="info-label">下行字节数</div>
|
||
<div class="info-value">{{ totalTrafficInfo.rx_bytes?.toLocaleString() || '0' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">上行字节数</div>
|
||
<div class="info-value">{{ totalTrafficInfo.tx_bytes?.toLocaleString() || '0' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 错误时显示的替代卡片 -->
|
||
<div class="info-card total-traffic-info" v-if="isTotalTrafficError">
|
||
<div class="card-title">
|
||
<el-icon><Monitor /></el-icon>
|
||
<span>历史总流量统计</span>
|
||
<el-tag
|
||
:type="TypeData === 'dockerContainer' ? 'info' : 'warning'"
|
||
size="small"
|
||
style="margin-left: 8px;"
|
||
>
|
||
{{ TypeData === 'dockerContainer' ? '不支持' : '不可用' }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="info-content">
|
||
<div class="info-item">
|
||
<div class="info-label">状态</div>
|
||
<div class="info-value error-status">{{ totalTrafficErrorMessage }}</div>
|
||
</div>
|
||
|
||
<template v-if="TypeData === 'dockerContainer'">
|
||
<div class="info-item">
|
||
<div class="info-label">说明</div>
|
||
<div class="info-value">容器服务器架构特性,不支持历史流量累计统计</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">替代方案</div>
|
||
<div class="info-value">请查看上方的"流量信息"卡片获取实时速率监控</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<div class="info-item">
|
||
<div class="info-label">说明</div>
|
||
<div class="info-value">可能原因:API接口配置问题、CORS策略限制或服务器维护中</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">建议</div>
|
||
<div class="info-value">请联系系统管理员或稍后重试</div>
|
||
</div>
|
||
</template>
|
||
</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>
|
||
<div class="header-actions">
|
||
<el-input
|
||
v-model="containerBox.key"
|
||
placeholder="搜索容器..."
|
||
class="search-input"
|
||
:prefix-icon="Search"
|
||
clearable
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
@click="showAddContainerDialog"
|
||
:icon="Plus"
|
||
>
|
||
添加容器
|
||
</el-button>
|
||
</div>
|
||
</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="name" 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
|
||
? 'warning'
|
||
: scope.row.container_state == 1
|
||
? 'info'
|
||
: scope.row.container_state == 2
|
||
? 'success'
|
||
: scope.row.container_state == 3
|
||
? 'info'
|
||
: scope.row.container_state == 4
|
||
? 'danger'
|
||
: 'info'"
|
||
effect="light"
|
||
>
|
||
{{
|
||
scope.row.container_state == 0
|
||
? "未支付"
|
||
: scope.row.container_state == 1
|
||
? "未构建"
|
||
: scope.row.container_state == 2
|
||
? "已构建"
|
||
: scope.row.container_state == 3
|
||
? "未知"
|
||
: scope.row.container_state == 4
|
||
? "已删除"
|
||
: "未知状态"
|
||
}}
|
||
</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/container?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"
|
||
min="1"
|
||
>
|
||
<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" :value="spec_form.port_num || 5">
|
||
<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" :value="spec_form.snapshot_num || 3">
|
||
<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" :value="spec_form.min_iops || 1000">
|
||
<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" :value="spec_form.max_iops || 5000">
|
||
<template #append>IOPS</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="form-section" v-if="TypeData == 'hyperV'">
|
||
<div class="section-title">GPU配置</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="GPU资源百分比">
|
||
<el-input v-model="spec_form.gpu_percentage" placeholder="GPU资源百分比" 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_partition_vram" 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.max_partition_vram" 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.optimal_partition_vram" placeholder="最优显存" type="number">
|
||
<template #append>MB</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="低内存映射IO空间">
|
||
<el-input v-model="spec_form.low_mmio_space" placeholder="默认128" type="number" :value="spec_form.low_mmio_space || 128">
|
||
<template #append>MB</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="高内存映射IO空间">
|
||
<el-input v-model="spec_form.high_mmio_space" placeholder="建议为显存的2倍" type="number">
|
||
<template #append>MB</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>
|
||
|
||
<!-- 添加容器对话框 -->
|
||
<el-dialog
|
||
v-model="addContainerDialogVisible"
|
||
title="添加容器"
|
||
width="750px"
|
||
destroy-on-close
|
||
class="container-dialog"
|
||
>
|
||
<!-- 服务器状态信息 -->
|
||
<div class="server-status-info">
|
||
<el-alert
|
||
:title="`服务器状态: ${serverMessage.state == 0 ? '离线' : '在线'}`"
|
||
:type="serverMessage.state == 0 ? 'warning' : 'success'"
|
||
:closable="false"
|
||
show-icon
|
||
>
|
||
<template #default>
|
||
<div class="status-details">
|
||
<p><strong>服务器名称:</strong> {{ serverMessage.name || '未设置' }}</p>
|
||
<p><strong>服务器IP:</strong> {{ serverMessage.server_ip || '未设置' }}</p>
|
||
<p><strong>服务器ID:</strong> {{ serverMessage.server_id || '未知' }}</p>
|
||
<p v-if="serverMessage.state == 0" class="status-warning">
|
||
<el-icon><WarningFilled /></el-icon>
|
||
注意:服务器离线时创建的容器可能无法正常启动
|
||
</p>
|
||
</div>
|
||
</template>
|
||
</el-alert>
|
||
</div>
|
||
|
||
<el-form
|
||
ref="addContainerFormRef"
|
||
:model="addContainerForm"
|
||
:rules="addContainerRules"
|
||
label-width="140px"
|
||
>
|
||
<div class="form-section">
|
||
<div class="section-title">基本信息</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="用户ID" prop="user_id">
|
||
<el-input
|
||
v-model="addContainerForm.user_id"
|
||
placeholder="请输入用户ID"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="容器套餐" prop="plan_id">
|
||
<el-select
|
||
v-model="addContainerForm.plan_id"
|
||
placeholder="请选择容器套餐"
|
||
style="width: 100%"
|
||
clearable
|
||
:loading="containerPlanLoading"
|
||
@focus="fetchContainerPlanList"
|
||
@change="handlePlanChange"
|
||
>
|
||
<el-option
|
||
v-for="plan in containerPlanList"
|
||
:key="plan.plan_id"
|
||
:label="plan.name"
|
||
:value="plan.plan_id"
|
||
>
|
||
<span style="float: left">{{ plan.name }}</span>
|
||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||
¥{{ plan.price }}/月
|
||
</span>
|
||
</el-option>
|
||
</el-select>
|
||
</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="购买价格" prop="pay">
|
||
<el-input-number
|
||
v-model="addContainerForm.pay"
|
||
:min="0"
|
||
:max="999999"
|
||
:precision="2"
|
||
placeholder="请输入购买价格"
|
||
style="width: 100%"
|
||
controls-position="right"
|
||
readonly
|
||
>
|
||
<template #prepend>¥</template>
|
||
</el-input-number>
|
||
<div v-if="selectedPlanPrice && addContainerForm.pay_months" class="price-calculation">
|
||
<small class="text-muted">
|
||
{{ selectedPlanPrice }}/月 × {{ addContainerForm.pay_months }}月 = ¥{{ addContainerForm.pay }}
|
||
</small>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="购买时间(月)" prop="pay_months">
|
||
<el-input-number
|
||
v-model="addContainerForm.pay_months"
|
||
:min="1"
|
||
:max="120"
|
||
placeholder="请输入购买月数"
|
||
style="width: 100%"
|
||
controls-position="right"
|
||
@change="calculateExpireTime"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="到期时间" prop="expire_time">
|
||
<el-date-picker
|
||
v-model="addContainerForm.expire_time"
|
||
type="datetime"
|
||
placeholder="请选择到期时间"
|
||
style="width: 100%"
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
value-format="X"
|
||
/>
|
||
</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="镜像" prop="image_id">
|
||
<el-select
|
||
v-model="addContainerForm.image_id"
|
||
placeholder="请选择镜像"
|
||
clearable
|
||
:loading="containerMirrorLoading"
|
||
@focus="fetchContainerMirrorList"
|
||
>
|
||
<el-option
|
||
v-for="mirror in containerMirrorList"
|
||
:key="mirror.id"
|
||
:label="mirror.show_name"
|
||
:value="mirror.image_id"
|
||
>
|
||
<span style="float: left">{{ mirror.show_name }}</span>
|
||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||
{{ mirror.size }}MB
|
||
</span>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="支付类型" prop="pay_type">
|
||
<el-select
|
||
v-model="addContainerForm.pay_type"
|
||
placeholder="请选择支付类型"
|
||
style="width: 100%"
|
||
clearable
|
||
>
|
||
<el-option label="余额" value="0" />
|
||
<!-- <el-option label="支付宝" value="alipay" />
|
||
<el-option label="微信支付" value="wechat" />
|
||
<el-option label="银行卡" value="bank" /> -->
|
||
</el-select>
|
||
</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="网络类型" prop="networkType">
|
||
<el-select
|
||
v-model="addContainerForm.networkType"
|
||
placeholder="请选择网络类型"
|
||
style="width: 100%"
|
||
@change="handleNetworkTypeChange"
|
||
>
|
||
<el-option label="无网络配置" value="" />
|
||
<el-option label="端口转发" value="port_forward" />
|
||
<el-option label="反向代理" value="nginx" />
|
||
<el-option label="浮动IP" value="floating_ip" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12" v-if="addContainerForm.networkType === 'port_forward' || addContainerForm.networkType === 'nginx'">
|
||
<el-form-item label="容器端口" prop="containerPort">
|
||
<el-input
|
||
v-model="addContainerForm.containerPort"
|
||
:min="1"
|
||
:max="65535"
|
||
placeholder="请输入容器端口"
|
||
style="width: 100%"
|
||
controls-position="right"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24" v-if="addContainerForm.networkType === 'nginx'">
|
||
<el-form-item label="域名" prop="domain">
|
||
<el-input
|
||
v-model="addContainerForm.domain"
|
||
placeholder="请输入域名"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24" v-if="addContainerForm.networkType === 'floating_ip'">
|
||
<el-alert
|
||
title="浮动IP配置"
|
||
type="info"
|
||
description="浮动IP类型无需额外配置参数,系统将自动分配可用的浮动IP"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<div class="section-title">环境配置</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="环境变量" prop="env">
|
||
<el-input
|
||
v-model="addContainerForm.env"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入环境变量(可选),格式:KEY1=VALUE1,KEY2=VALUE2"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="cancelAddContainer">取消</el-button>
|
||
<el-button type="primary" @click="confirmAddContainer" :loading="addContainerLoading">
|
||
确定
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref, onMounted, watch, computed } from "vue";
|
||
import { useRouter, useRoute } from "vue-router";
|
||
import {
|
||
selectServer,
|
||
getServerPlan,
|
||
editServerPlan,
|
||
addServerPlan,
|
||
deleteServerPlan,
|
||
getContainer,
|
||
getFloatingIpList,
|
||
addFloatingIp,
|
||
delFloatingIp,
|
||
addFloatingIpBatch,
|
||
selectServerPlan,
|
||
getDiskInfo,
|
||
getRealDisk,
|
||
getTraffic,
|
||
getTotalTraffic,
|
||
addContainer
|
||
} from "@/utils/acs/server";
|
||
import { getMirrorList } from "@/utils/acs/mirror";
|
||
import { ElMessage, ElNotification } from 'element-plus';
|
||
import { copyDomText } from "@/utils/hide";
|
||
import { getUserInfoV1 } from "@/utils/acs/user";
|
||
import {getUserInfo, userLogin} from "@/api/login.js";
|
||
import {
|
||
Plus,
|
||
Edit,
|
||
Delete,
|
||
Download,
|
||
Upload,
|
||
Refresh,
|
||
Monitor,
|
||
Cpu as CpuIcon,
|
||
Location,
|
||
Setting,
|
||
Search,
|
||
ArrowLeft,
|
||
CircleCheckFilled,
|
||
Files,
|
||
Box,
|
||
PieChart,
|
||
FolderOpened,
|
||
DataAnalysis,
|
||
WarningFilled
|
||
} 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 diskInfo = ref([]); // 修改为数组,因为API返回的是磁盘设备数组
|
||
const realDiskInfo = ref({});
|
||
const trafficInfo = ref({});
|
||
const totalTrafficInfo = ref({});
|
||
const loading = ref(false);
|
||
const isTotalTrafficError = ref(false);
|
||
const totalTrafficErrorMessage = ref('');
|
||
|
||
// 返回按钮功能
|
||
const goBack = () => {
|
||
// 标记返回操作
|
||
sessionStorage.setItem('serverDetailFrom', 'back');
|
||
sessionStorage.setItem('serverDetailTimestamp', Date.now().toString());
|
||
router.back();
|
||
};
|
||
|
||
// 格式化文件大小显示
|
||
const formatSize = (sizeInGB) => {
|
||
if (typeof sizeInGB !== 'number') return '0';
|
||
return sizeInGB.toFixed(2);
|
||
};
|
||
|
||
// 根据使用率获取进度条颜色
|
||
const getUsageColor = (percent) => {
|
||
if (percent >= 90) return '#F56C6C'; // 红色
|
||
if (percent >= 80) return '#E6A23C'; // 橙色
|
||
if (percent >= 60) return '#409EFF'; // 蓝色
|
||
return '#67C23A'; // 绿色
|
||
};
|
||
|
||
// 新增的辅助函数
|
||
const getTotalCapacity = () => {
|
||
if (!diskInfo.value || !Array.isArray(diskInfo.value)) return '0.00';
|
||
const total = diskInfo.value.reduce((sum, disk) => sum + (disk.total_gb || 0), 0);
|
||
return formatSize(total);
|
||
};
|
||
|
||
const getTotalUsed = () => {
|
||
if (!diskInfo.value || !Array.isArray(diskInfo.value)) return '0.00';
|
||
const used = diskInfo.value.reduce((sum, disk) => sum + (disk.used_gb || 0), 0);
|
||
return formatSize(used);
|
||
};
|
||
|
||
const getAverageUsage = () => {
|
||
if (!diskInfo.value || !Array.isArray(diskInfo.value) || diskInfo.value.length === 0) return 0;
|
||
const avgUsage = diskInfo.value.reduce((sum, disk) => sum + (disk.percent || 0), 0) / diskInfo.value.length;
|
||
return parseFloat(avgUsage.toFixed(1));
|
||
};
|
||
|
||
const getDeviceStatusType = (percent) => {
|
||
if (percent >= 90) return 'danger';
|
||
if (percent >= 80) return 'warning';
|
||
if (percent >= 60) return 'info';
|
||
return 'success';
|
||
};
|
||
|
||
const getDeviceStatusText = (percent) => {
|
||
if (percent >= 90) return '严重';
|
||
if (percent >= 80) return '警告';
|
||
if (percent >= 60) return '注意';
|
||
return '正常';
|
||
};
|
||
|
||
const getUsageClass = (percent) => {
|
||
if (percent >= 90) return 'usage-critical';
|
||
if (percent >= 80) return 'usage-warning';
|
||
if (percent >= 60) return 'usage-medium';
|
||
return 'usage-normal';
|
||
};
|
||
|
||
const getUsageIconClass = (percent) => {
|
||
if (percent >= 90) return 'critical';
|
||
if (percent >= 80) return 'warning';
|
||
if (percent >= 60) return 'medium';
|
||
return 'normal';
|
||
};
|
||
|
||
// 格式化实际硬件划分数值
|
||
const formatRealDiskValue = (value) => {
|
||
if (typeof value === 'number') {
|
||
return value.toFixed(0); // 显示整数
|
||
}
|
||
if (typeof value === 'string' && !isNaN(value)) {
|
||
return parseFloat(value).toFixed(0);
|
||
}
|
||
return value || '0';
|
||
};
|
||
|
||
// 格式化流量字节数
|
||
const formatTrafficBytes = (bytes, unit = 'bytes') => {
|
||
if (typeof bytes !== 'number') return '0';
|
||
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
if (bytes === 0) return '0 B';
|
||
|
||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||
const formattedSize = (bytes / Math.pow(1024, i)).toFixed(2);
|
||
|
||
return `${formattedSize} ${sizes[i]}`;
|
||
};
|
||
|
||
// 格式化网络速率
|
||
const formatNetworkSpeed = (mbps) => {
|
||
if (typeof mbps !== 'number') return '0 Mbps';
|
||
|
||
if (mbps < 1) {
|
||
return `${(mbps * 1000).toFixed(2)} Kbps`;
|
||
} else if (mbps >= 1000) {
|
||
return `${(mbps / 1000).toFixed(2)} Gbps`;
|
||
} else {
|
||
return `${mbps.toFixed(2)} Mbps`;
|
||
}
|
||
};
|
||
|
||
// 格式化流量字节数为GB显示
|
||
const formatTrafficBytesToGB = (bytes) => {
|
||
if (typeof bytes !== 'number' || bytes === 0) return '0.00';
|
||
|
||
const gb = bytes / (1024 * 1024 * 1024);
|
||
if (gb < 0.01) {
|
||
// 小于0.01GB时显示MB
|
||
const mb = bytes / (1024 * 1024);
|
||
return mb < 0.01 ? '< 0.01 MB' : `${mb.toFixed(2)} MB`;
|
||
} else if (gb >= 1024) {
|
||
// 大于1024GB时显示TB
|
||
const tb = gb / 1024;
|
||
return `${tb.toFixed(2)} TB`;
|
||
} else {
|
||
return `${gb.toFixed(2)} GB`;
|
||
}
|
||
};
|
||
|
||
// 计算平均每日流量(这里是一个简化的计算,实际可能需要更复杂的逻辑)
|
||
const calculateAvgDaily = (totalBytes) => {
|
||
if (typeof totalBytes !== 'number' || totalBytes === 0) return '0.00 GB';
|
||
|
||
// 假设按30天计算平均值(实际应该根据服务器运行时间计算)
|
||
const avgDaily = totalBytes / 30;
|
||
return formatTrafficBytesToGB(avgDaily);
|
||
};
|
||
|
||
// 分析流量趋势
|
||
const getTotalTrafficTrend = (rxBytes, txBytes) => {
|
||
if (typeof rxBytes !== 'number' || typeof txBytes !== 'number') return '无数据';
|
||
|
||
const total = rxBytes + txBytes;
|
||
if (total === 0) return '无流量';
|
||
|
||
const rxPercent = (rxBytes / total) * 100;
|
||
const txPercent = (txBytes / total) * 100;
|
||
|
||
if (rxPercent > 70) {
|
||
return '下行为主';
|
||
} else if (txPercent > 70) {
|
||
return '上行为主';
|
||
} else {
|
||
return '均衡使用';
|
||
}
|
||
};
|
||
|
||
// 初始化数据的函数
|
||
const initData = async () => {
|
||
try {
|
||
loading.value = true;
|
||
TypeData.value = route.query.type;
|
||
// 重置数据状态
|
||
serverMessage.value = {};
|
||
spec_list.value = [];
|
||
user_servers.value = [];
|
||
floatList.value = [];
|
||
diskInfo.value = []; // 重置为空数组
|
||
realDiskInfo.value = {};
|
||
trafficInfo.value = {};
|
||
totalTrafficInfo.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
|
||
});
|
||
}
|
||
|
||
// 并行获取服务器硬件和流量信息(不阻塞主要功能)
|
||
Promise.allSettled([
|
||
|
||
getDiskInfoData(),
|
||
getRealDiskData(),
|
||
getTrafficData(),
|
||
getTotalTrafficData()
|
||
]).then(results => {
|
||
const failedRequests = results.filter(result => result.status === 'rejected');
|
||
if (failedRequests.length > 0) {
|
||
console.warn(`${failedRequests.length} 个API请求失败,但不影响主要功能`);
|
||
}
|
||
});
|
||
|
||
// 获取浮动IP列表
|
||
await getFloating();
|
||
|
||
// 获取实例规格
|
||
await GetSpecs();
|
||
|
||
// 获取用户信息并设置用户类型
|
||
try {
|
||
const userInfoRes = await getUserInfo();
|
||
if (userInfoRes && userInfoRes.data ) {
|
||
localStorage.setItem("user_id", userInfoRes.data.user_id);
|
||
}
|
||
} catch (error) {
|
||
console.error("获取用户信息失败:", error);
|
||
}
|
||
|
||
// 获取所有容器
|
||
containerBox.server_id = route.query.server_id;
|
||
containerBox.user_id = localStorage.getItem("user_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("加载数据失败,请刷新页面重试");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 监听路由参数变化
|
||
watch(
|
||
() => route.query,
|
||
(newQuery) => {
|
||
if (newQuery.server_id) {
|
||
initData();
|
||
}
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
onMounted(() => {
|
||
initData();
|
||
});
|
||
|
||
const TypeData = ref("");
|
||
|
||
// 获取服务器硬件信息
|
||
const getDiskInfoData = async () => {
|
||
try {
|
||
const res = await getDiskInfo(route.query.server_id);
|
||
console.log("获取服务器硬件信息完整响应:", res);
|
||
|
||
if (res && res.data) {
|
||
console.log("响应数据结构:", res.data);
|
||
|
||
// 检查多层嵌套结构
|
||
let diskData = null;
|
||
|
||
if (res.data.code === 200) {
|
||
// 容器服务器:检查 res.data.data (单个对象)
|
||
if (TypeData.value === 'dockerContainer') {
|
||
if (res.data.data && typeof res.data.data === 'object' && !Array.isArray(res.data.data)) {
|
||
// 容器服务器返回单个磁盘对象,转换为数组格式以统一处理
|
||
const containerDisk = res.data.data.data;
|
||
diskData = [{
|
||
device: containerDisk.path || '/var/lib/docker',
|
||
total_gb: (containerDisk.total || 0) / (1024 * 1024 * 1024),
|
||
used_gb: (containerDisk.used || 0) / (1024 * 1024 * 1024),
|
||
free_gb: (containerDisk.free || 0) / (1024 * 1024 * 1024),
|
||
percent: containerDisk.total > 0 ? ((containerDisk.used / containerDisk.total) * 100).toFixed(1) : 0
|
||
}];
|
||
console.log("容器服务器磁盘数据:", diskData);
|
||
}
|
||
}
|
||
// 虚拟机服务器:检查数组结构
|
||
else {
|
||
// 检查 res.data.data.data (双层data嵌套)
|
||
if (res.data.data && res.data.data.data && Array.isArray(res.data.data.data)) {
|
||
diskData = res.data.data.data;
|
||
console.log("虚拟机服务器-双层data嵌套的数组数据:", diskData);
|
||
}
|
||
// 检查 res.data.data (单层data嵌套)
|
||
else if (res.data.data && Array.isArray(res.data.data)) {
|
||
diskData = res.data.data;
|
||
console.log("虚拟机服务器-单层data嵌套的数组数据:", diskData);
|
||
}
|
||
// 检查直接在res.data下的数组
|
||
else if (Array.isArray(res.data)) {
|
||
diskData = res.data;
|
||
console.log("虚拟机服务器-直接数组数据:", diskData);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (diskData && Array.isArray(diskData) && diskData.length > 0) {
|
||
diskInfo.value = diskData;
|
||
console.log("设置硬件信息数据:", diskData);
|
||
} else {
|
||
diskInfo.value = '暂无数据';
|
||
console.log("未找到有效的硬件数据");
|
||
}
|
||
} else {
|
||
diskInfo.value = '响应格式错误';
|
||
console.log("响应格式不正确");
|
||
}
|
||
} catch (error) {
|
||
console.error("获取服务器硬件信息失败:", error);
|
||
diskInfo.value = '接口异常';
|
||
}
|
||
};
|
||
|
||
// 获取服务器实际划分硬件信息
|
||
const getRealDiskData = async () => {
|
||
try {
|
||
const res = await getRealDisk(route.query.server_id);
|
||
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
const data = res.data.data;
|
||
|
||
|
||
if (data && typeof data === 'object') {
|
||
// 处理新的数据结构
|
||
realDiskInfo.value = {
|
||
allocated_disk: data.desk || 0,
|
||
unit: data.unit || 'GB',
|
||
// 如果将来有其他字段,可以在这里添加
|
||
allocated_cpu: data.cpu || '暂无数据',
|
||
allocated_memory: data.memory || '暂无数据',
|
||
remaining_resources: data.remaining || '暂无数据'
|
||
};
|
||
} else {
|
||
realDiskInfo.value = {
|
||
allocated_disk: '暂无数据',
|
||
unit: 'GB',
|
||
allocated_cpu: '暂无数据',
|
||
allocated_memory: '暂无数据',
|
||
remaining_resources: '暂无数据'
|
||
};
|
||
}
|
||
} else {
|
||
realDiskInfo.value = {
|
||
allocated_disk: '暂无数据',
|
||
unit: 'GB',
|
||
allocated_cpu: '暂无数据',
|
||
allocated_memory: '暂无数据',
|
||
remaining_resources: '暂无数据'
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error("获取服务器实际硬件信息失败:", error);
|
||
realDiskInfo.value = {
|
||
allocated_disk: '接口异常',
|
||
unit: 'GB',
|
||
allocated_cpu: '接口异常',
|
||
allocated_memory: '接口异常',
|
||
remaining_resources: '接口异常'
|
||
};
|
||
}
|
||
};
|
||
|
||
// 获取服务器流量信息
|
||
const getTrafficData = async () => {
|
||
try {
|
||
const res = await getTraffic(route.query.server_id);
|
||
console.log("获取服务器流量信息完整响应:", res);
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
// 处理不同服务器类型的数据结构
|
||
let trafficData = null;
|
||
|
||
if (TypeData.value === 'dockerContainer') {
|
||
// 容器服务器:检查 res.data.data (直接对象)
|
||
if (res.data.data && typeof res.data.data === 'object') {
|
||
trafficData = res.data.data.data;
|
||
console.log("容器服务器流量数据:", trafficData);
|
||
}
|
||
} else {
|
||
// 虚拟机服务器:处理双层嵌套的数据结构
|
||
if (res.data.data && res.data.data.code === 200 && res.data.data.data) {
|
||
trafficData = res.data.data.data;
|
||
} else if (res.data.data) {
|
||
trafficData = res.data.data;
|
||
}
|
||
console.log("虚拟机服务器流量数据:", trafficData);
|
||
}
|
||
|
||
if (trafficData && typeof trafficData === 'object') {
|
||
if (TypeData.value === 'dockerContainer') {
|
||
// 容器服务器数据格式: {recv: "7.15", sent: "1.14", unit: "KB/s"}
|
||
trafficInfo.value = {
|
||
// 实时速率 (容器返回的是速率数据)
|
||
rx_speed: parseFloat(trafficData.recv) || 0, // 下行速率
|
||
tx_speed: parseFloat(trafficData.sent) || 0, // 上行速率
|
||
unit: trafficData.unit || 'KB/s', // 速率单位
|
||
|
||
// 格式化显示
|
||
rx_speed_display: `${trafficData.recv || '0'} ${trafficData.unit || 'KB/s'}`,
|
||
tx_speed_display: `${trafficData.sent || '0'} ${trafficData.unit || 'KB/s'}`,
|
||
|
||
// 容器没有累计流量数据,设为不可用
|
||
rx_total: '不支持',
|
||
tx_total: '不支持',
|
||
today_traffic: '实时监控',
|
||
upload_traffic: `${trafficData.sent || '0'} ${trafficData.unit || 'KB/s'}`,
|
||
download_traffic: `${trafficData.recv || '0'} ${trafficData.unit || 'KB/s'}`
|
||
};
|
||
} else {
|
||
// 虚拟机服务器数据格式 (原有逻辑)
|
||
trafficInfo.value = {
|
||
// 流量总量
|
||
rx_total: trafficData.rx || 0, // 下行总流量 bytes
|
||
tx_total: trafficData.tx || 0, // 上行总流量 bytes
|
||
unit: trafficData.unit || 'bytes', // 单位
|
||
|
||
// 实时速率
|
||
rx_speed: trafficData.mbps?.rx || 0, // 下行速率 Mbps
|
||
tx_speed: trafficData.mbps?.tx || 0, // 上行速率 Mbps
|
||
|
||
// 为兼容性保留原字段
|
||
today_traffic: formatTrafficBytes(trafficData.rx + trafficData.tx, trafficData.unit),
|
||
upload_traffic: formatTrafficBytes(trafficData.tx, trafficData.unit),
|
||
download_traffic: formatTrafficBytes(trafficData.rx, trafficData.unit)
|
||
};
|
||
}
|
||
} else {
|
||
trafficInfo.value = {
|
||
rx_total: '暂无数据',
|
||
tx_total: '暂无数据',
|
||
rx_speed: '暂无数据',
|
||
tx_speed: '暂无数据',
|
||
unit: 'bytes',
|
||
today_traffic: '暂无数据',
|
||
upload_traffic: '暂无数据',
|
||
download_traffic: '暂无数据'
|
||
};
|
||
}
|
||
} else {
|
||
trafficInfo.value = {
|
||
rx_total: '暂无数据',
|
||
tx_total: '暂无数据',
|
||
rx_speed: '暂无数据',
|
||
tx_speed: '暂无数据',
|
||
unit: 'bytes',
|
||
today_traffic: '暂无数据',
|
||
upload_traffic: '暂无数据',
|
||
download_traffic: '暂无数据'
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error("获取服务器流量信息失败:", error);
|
||
trafficInfo.value = {
|
||
rx_total: '接口异常',
|
||
tx_total: '接口异常',
|
||
rx_speed: '接口异常',
|
||
tx_speed: '接口异常',
|
||
unit: 'bytes',
|
||
today_traffic: '接口异常',
|
||
upload_traffic: '接口异常',
|
||
download_traffic: '接口异常'
|
||
};
|
||
}
|
||
};
|
||
|
||
// 获取服务器总流量信息
|
||
const getTotalTrafficData = async () => {
|
||
try {
|
||
isTotalTrafficError.value = false;
|
||
totalTrafficErrorMessage.value = '';
|
||
|
||
// 容器服务器不支持总流量统计
|
||
if (TypeData.value === 'dockerContainer') {
|
||
console.log("容器服务器不支持总流量统计功能");
|
||
isTotalTrafficError.value = true;
|
||
totalTrafficErrorMessage.value = '容器服务器不支持此功能';
|
||
return;
|
||
}
|
||
|
||
const res = await getTotalTraffic(route.query.server_id);
|
||
console.log("获取服务器总流量信息完整响应:", res);
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
// 处理嵌套的数据结构:res.data.data.data
|
||
let trafficData = null;
|
||
|
||
if (res.data.data && res.data.data.code === 200 && res.data.data.data) {
|
||
trafficData = res.data.data.data;
|
||
} else if (res.data.data) {
|
||
trafficData = res.data.data;
|
||
}
|
||
|
||
console.log("解析到的总流量数据:", trafficData);
|
||
|
||
if (trafficData && typeof trafficData === 'object') {
|
||
// 根据实际返回的数据结构处理
|
||
const rxBytes = trafficData.rx || 0; // 下行流量 bytes
|
||
const txBytes = trafficData.tx || 0; // 上行流量 bytes
|
||
const totalBytes = rxBytes + txBytes; // 总流量
|
||
const unit = trafficData.unit || 'bytes'; // 单位
|
||
|
||
totalTrafficInfo.value = {
|
||
// 原始数据
|
||
rx_bytes: rxBytes,
|
||
tx_bytes: txBytes,
|
||
total_bytes: totalBytes,
|
||
unit: unit,
|
||
|
||
// 格式化后的显示数据
|
||
total_traffic: formatTrafficBytesToGB(totalBytes),
|
||
download_traffic: formatTrafficBytesToGB(rxBytes),
|
||
upload_traffic: formatTrafficBytesToGB(txBytes),
|
||
|
||
// 计算一些统计数据(这里可以根据需要添加更多计算逻辑)
|
||
avg_daily_traffic: calculateAvgDaily(totalBytes),
|
||
peak_traffic: formatTrafficBytesToGB(Math.max(rxBytes, txBytes)),
|
||
traffic_trend: getTotalTrafficTrend(rxBytes, txBytes)
|
||
};
|
||
} else {
|
||
// 数据格式不正确时的默认值
|
||
totalTrafficInfo.value = {
|
||
total_traffic: '暂无数据',
|
||
download_traffic: '暂无数据',
|
||
upload_traffic: '暂无数据',
|
||
avg_daily_traffic: '暂无数据',
|
||
peak_traffic: '暂无数据',
|
||
traffic_trend: '暂无数据'
|
||
};
|
||
}
|
||
} else {
|
||
// API返回非200状态码,设置默认值
|
||
totalTrafficInfo.value = {
|
||
total_traffic: '暂无数据',
|
||
download_traffic: '暂无数据',
|
||
upload_traffic: '暂无数据',
|
||
avg_daily_traffic: '暂无数据',
|
||
peak_traffic: '暂无数据',
|
||
traffic_trend: '暂无数据'
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error("获取服务器总流量信息失败:", error);
|
||
|
||
// 设置错误状态
|
||
isTotalTrafficError.value = true;
|
||
|
||
// 改进错误类型检测
|
||
if (error.message?.includes('CORS') ||
|
||
error.message?.includes('blocked') ||
|
||
error.name === 'TypeError' ||
|
||
error.code === 'ERR_NETWORK' ||
|
||
(error.response && error.response.status === 0)) {
|
||
console.warn("CORS或网络错误,显示友好提示");
|
||
totalTrafficErrorMessage.value = '接口暂时不可用 (网络限制)';
|
||
} else if (error.response?.status === 404) {
|
||
totalTrafficErrorMessage.value = '总流量接口不存在';
|
||
} else if (error.response?.status >= 500) {
|
||
totalTrafficErrorMessage.value = '服务器内部错误';
|
||
} else if (error.response?.status === 403) {
|
||
totalTrafficErrorMessage.value = '权限不足';
|
||
} else {
|
||
totalTrafficErrorMessage.value = '接口暂不可用';
|
||
}
|
||
|
||
// 不显示错误消息,避免打扰用户
|
||
console.warn(`总流量API请求失败: ${totalTrafficErrorMessage.value}`, error);
|
||
}
|
||
};
|
||
|
||
//获取服务器实例规格
|
||
const GetSpecs = async () => {
|
||
specLoading.value = true;
|
||
try {
|
||
let plans = await getServerPlan({
|
||
server_id: route.query.server_id,
|
||
count: 30
|
||
});
|
||
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,
|
||
server_type:TypeData.value
|
||
});
|
||
|
||
|
||
if (res.data.code == 200) {
|
||
ElNotification({
|
||
title: '添加成功',
|
||
message: `已成功添加规格 "${spec_form.name}"`,
|
||
type: 'success',
|
||
duration: 3000
|
||
});
|
||
centerDialogVisible.value = false;
|
||
} else {
|
||
ElMessage.error(res.data.msg || "添加失败");
|
||
return;
|
||
}
|
||
} else {
|
||
const submitData = {
|
||
...spec_form,
|
||
server_id:route.query.server_id,
|
||
server_type:TypeData.value
|
||
}
|
||
|
||
// 处理虚拟机特有字段
|
||
if (TypeData.value === 'hyperV') {
|
||
// 确保数值类型字段有默认值
|
||
submitData.port_num = submitData.port_num || 5;
|
||
submitData.snapshot_num = submitData.snapshot_num || 3;
|
||
submitData.min_iops = submitData.min_iops || 1000;
|
||
submitData.max_iops = submitData.max_iops || 5000;
|
||
|
||
// 容器特有字段设为空
|
||
submitData.bandwidth_rx = '0';
|
||
submitData.bandwidth_tx = '0';
|
||
submitData.threshold_rx = '0';
|
||
submitData.threshold_tx = '0';
|
||
} else {
|
||
// 处理容器特有字段
|
||
submitData.bandwidth_rx = submitData.bandwidth_rx || '100';
|
||
submitData.bandwidth_tx = submitData.bandwidth_tx || '100';
|
||
}
|
||
|
||
const res = await editServerPlan(submitData);
|
||
|
||
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: 0,
|
||
snapshot_num: "",
|
||
number: "",
|
||
volume_price: "",
|
||
min_iops: "",
|
||
max_iops: "",
|
||
description: "",
|
||
must_real_name: 0,
|
||
gpu_percentage: "",
|
||
min_partition_vram: "",
|
||
max_partition_vram: "",
|
||
optimal_partition_vram: "",
|
||
low_mmio_space: "",
|
||
high_mmio_space: ""
|
||
});
|
||
|
||
function show_spec(data = null) {
|
||
if (!data) {
|
||
// 重置表单时根据服务器类型设置不同默认值
|
||
Object.keys(spec_form).forEach(key => {
|
||
spec_form[key] = key === 'must_real_name' ? 0 : '';
|
||
});
|
||
|
||
// 设置服务器类型和类型相关默认值
|
||
spec_form.server_type = TypeData.value;
|
||
|
||
if (TypeData.value === 'dockerContainer') {
|
||
spec_form.bandwidth_rx = '100';
|
||
spec_form.bandwidth_tx = '100';
|
||
} else {
|
||
// 虚拟机默认值
|
||
spec_form.port_num = '5';
|
||
spec_form.snapshot_num = '3';
|
||
spec_form.min_iops = '1000';
|
||
spec_form.max_iops = '5000';
|
||
spec_form.low_mmio_space = '128';
|
||
}
|
||
} else {
|
||
// 复制传入的数据
|
||
Object.keys(data).forEach(key => {
|
||
if (key in spec_form) {
|
||
spec_form[key] = data[key];
|
||
}
|
||
});
|
||
// 确保服务器类型正确
|
||
spec_form.server_type = TypeData.value;
|
||
}
|
||
}
|
||
|
||
//复制内容
|
||
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("无法读取剪贴板内容,请检查格式是否正确");
|
||
}
|
||
};
|
||
|
||
// 添加容器相关状态
|
||
const addContainerDialogVisible = ref(false);
|
||
const addContainerLoading = ref(false);
|
||
const addContainerFormRef = ref(null);
|
||
const addContainerForm = ref({
|
||
user_id: '',
|
||
server_id: '',
|
||
plan_id: '',
|
||
pay: null,
|
||
expire_time: null,
|
||
image_id: '',
|
||
pay_months: null,
|
||
proxy: '',
|
||
pay_type: '',
|
||
env: '',
|
||
// 网络配置相关字段
|
||
networkType: '',
|
||
containerPort: null,
|
||
domain: ''
|
||
});
|
||
|
||
// 容器套餐和镜像数据
|
||
const containerPlanList = ref([]);
|
||
const containerMirrorList = ref([]);
|
||
const containerPlanLoading = ref(false);
|
||
const containerMirrorLoading = ref(false);
|
||
|
||
// 获取选中套餐的价格
|
||
const selectedPlanPrice = computed(() => {
|
||
if (!addContainerForm.value.plan_id) return null;
|
||
const selectedPlan = containerPlanList.value.find(plan => plan.plan_id === addContainerForm.value.plan_id);
|
||
return selectedPlan ? parseFloat(selectedPlan.price) : null;
|
||
});
|
||
|
||
// 容器表单验证规则 - 使用computed实现动态验证
|
||
const addContainerRules = computed(() => {
|
||
const rules = {
|
||
user_id: [
|
||
{ required: true, message: '请输入用户ID', trigger: 'blur' }
|
||
],
|
||
plan_id: [
|
||
{ required: true, message: '请选择容器套餐', trigger: 'change' }
|
||
],
|
||
pay: [
|
||
{ required: true, message: '请输入购买价格', trigger: 'blur' }
|
||
],
|
||
expire_time: [
|
||
{ required: true, message: '请选择到期时间', trigger: 'change' }
|
||
]
|
||
};
|
||
|
||
// 根据网络类型动态添加验证规则
|
||
if (addContainerForm.value.networkType === 'port_forward' || addContainerForm.value.networkType === 'nginx') {
|
||
rules.containerPort = [
|
||
{ required: true, message: '请输入容器端口', trigger: 'blur' },
|
||
{ pattern: /^[1-9]\d*$/, message: '请输入有效的正整数端口', trigger: 'blur' }, ];
|
||
}
|
||
|
||
if (addContainerForm.value.networkType === 'nginx') {
|
||
rules.domain = [
|
||
{ required: true, message: '请输入域名', trigger: 'blur' },
|
||
{ pattern: /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/, message: '请输入有效的域名', trigger: 'blur' }
|
||
];
|
||
}
|
||
|
||
return rules;
|
||
});
|
||
|
||
// 获取容器套餐列表
|
||
const fetchContainerPlanList = async () => {
|
||
if (containerPlanList.value.length > 0) return; // 已加载过,不重复加载
|
||
|
||
containerPlanLoading.value = true;
|
||
try {
|
||
const response = await getServerPlan({
|
||
server_id: route.query.server_id,
|
||
count: 100
|
||
});
|
||
console.log("获取容器套餐列表1111:",response);
|
||
|
||
if (response && response.data && response.data.code === 200) {
|
||
containerPlanList.value = response.data.data || [];
|
||
} else {
|
||
ElMessage.error('获取容器套餐列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取容器套餐列表出错:', error);
|
||
ElMessage.error('获取容器套餐列表出错');
|
||
} finally {
|
||
containerPlanLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取容器镜像列表
|
||
const fetchContainerMirrorList = async () => {
|
||
if (containerMirrorList.value.length > 0) return; // 已加载过,不重复加载
|
||
|
||
containerMirrorLoading.value = true;
|
||
try {
|
||
const response = await getMirrorList({server_id: route.query.server_id, page: 1, count: 999,key: '',class_id: ''});
|
||
console.log("获取镜像列表1111:",response);
|
||
|
||
if (response && response.data && response.data.code === 200) {
|
||
containerMirrorList.value = response.data.data || [];
|
||
} else {
|
||
ElMessage.error('获取镜像列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取镜像列表出错:', error);
|
||
ElMessage.error('获取镜像列表出错');
|
||
} finally {
|
||
containerMirrorLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 处理套餐变化
|
||
const handlePlanChange = (planId) => {
|
||
if (!planId) {
|
||
addContainerForm.value.pay = null;
|
||
return;
|
||
}
|
||
|
||
// 计算总价格和到期时间
|
||
calculateTotalPrice();
|
||
if (addContainerForm.value.pay_months) {
|
||
calculateExpireTime();
|
||
}
|
||
};
|
||
|
||
// 计算总价格
|
||
const calculateTotalPrice = () => {
|
||
if (!selectedPlanPrice.value || !addContainerForm.value.pay_months) {
|
||
addContainerForm.value.pay = selectedPlanPrice.value || null;
|
||
return;
|
||
}
|
||
|
||
// 计算总价格 = 套餐价格 × 购买月数
|
||
addContainerForm.value.pay = parseFloat((selectedPlanPrice.value * addContainerForm.value.pay_months).toFixed(2));
|
||
};
|
||
|
||
// 计算到期时间
|
||
const calculateExpireTime = () => {
|
||
if (!addContainerForm.value.pay_months) {
|
||
addContainerForm.value.expire_time = null;
|
||
return;
|
||
}
|
||
|
||
// 获取当前时间
|
||
const now = new Date();
|
||
|
||
// 计算到期时间(当前时间 + 购买月数)
|
||
const expireDate = new Date(now);
|
||
expireDate.setMonth(expireDate.getMonth() + addContainerForm.value.pay_months);
|
||
|
||
// 转换为时间戳(秒)
|
||
addContainerForm.value.expire_time = Math.floor(expireDate.getTime() / 1000);
|
||
|
||
// 同时重新计算总价格
|
||
calculateTotalPrice();
|
||
};
|
||
|
||
// 处理网络类型变化
|
||
const handleNetworkTypeChange = (newType) => {
|
||
// 清空网络配置相关字段
|
||
addContainerForm.value.containerPort = null;
|
||
addContainerForm.value.domain = '';
|
||
|
||
// 清除验证错误
|
||
if (addContainerFormRef.value) {
|
||
addContainerFormRef.value.clearValidate(['containerPort', 'domain']);
|
||
}
|
||
};
|
||
|
||
// 显示添加容器对话框
|
||
const showAddContainerDialog = () => {
|
||
addContainerDialogVisible.value = true;
|
||
// 重置表单
|
||
addContainerForm.value = {
|
||
user_id: '',
|
||
server_id: route.query.server_id,
|
||
plan_id: '',
|
||
pay: null,
|
||
expire_time: null,
|
||
image_id: '',
|
||
pay_months: null,
|
||
proxy: '',
|
||
pay_type: '',
|
||
env: '',
|
||
// 网络配置相关字段
|
||
networkType: '',
|
||
containerPort: null,
|
||
domain: ''
|
||
};
|
||
// 清除验证
|
||
if (addContainerFormRef.value) {
|
||
addContainerFormRef.value.clearValidate();
|
||
}
|
||
// 预加载数据
|
||
fetchContainerPlanList();
|
||
fetchContainerMirrorList();
|
||
};
|
||
|
||
// 取消添加容器
|
||
const cancelAddContainer = () => {
|
||
addContainerDialogVisible.value = false;
|
||
};
|
||
|
||
// 确认添加容器
|
||
const confirmAddContainer = async () => {
|
||
if (!addContainerFormRef.value) return;
|
||
|
||
try {
|
||
// 验证表单
|
||
await addContainerFormRef.value.validate();
|
||
|
||
addContainerLoading.value = true;
|
||
|
||
// 构建网络配置数据
|
||
let proxyData = '';
|
||
if (addContainerForm.value.networkType) {
|
||
let proxy_data = [{
|
||
type: addContainerForm.value.networkType
|
||
}];
|
||
|
||
// 根据类型添加对应参数
|
||
if (addContainerForm.value.networkType === 'port_forward') {
|
||
proxy_data[0].container_port = addContainerForm.value.containerPort;
|
||
} else if (addContainerForm.value.networkType === 'nginx') {
|
||
proxy_data[0].container_port = addContainerForm.value.containerPort;
|
||
proxy_data[0].domain = addContainerForm.value.domain;
|
||
}
|
||
// floating_ip 类型不需要额外参数
|
||
|
||
proxyData = JSON.stringify(proxy_data);
|
||
}
|
||
|
||
// 准备API参数
|
||
const apiParams = {
|
||
user_id: addContainerForm.value.user_id,
|
||
server_id: addContainerForm.value.server_id,
|
||
plan_id: addContainerForm.value.plan_id,
|
||
pay: addContainerForm.value.pay,
|
||
expire_time: addContainerForm.value.expire_time,
|
||
image_id: addContainerForm.value.image_id || '',
|
||
pay_months: addContainerForm.value.pay_months?.toString() || '',
|
||
proxy: proxyData,
|
||
pay_type: addContainerForm.value.pay_type || '',
|
||
env: addContainerForm.value.env || ''
|
||
};
|
||
|
||
console.log('添加容器参数:', apiParams);
|
||
|
||
// 调用API
|
||
const response = await addContainer(apiParams);
|
||
|
||
if (response && response.data && response.data.code === 200) {
|
||
ElMessage.success('容器创建成功');
|
||
addContainerDialogVisible.value = false;
|
||
// 刷新容器列表
|
||
containerBox.page = 1;
|
||
let cons = await getContainer(containerBox);
|
||
if (cons && cons.data) {
|
||
user_servers.value = cons.data.data || [];
|
||
total.value = cons.data.count || 0;
|
||
}
|
||
} else {
|
||
ElMessage.error('创建失败: ' + (response.data?.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('添加容器出错:', error);
|
||
if (error.message) {
|
||
ElMessage.error('表单验证失败: ' + error.message);
|
||
} else {
|
||
ElMessage.error('添加容器出错');
|
||
}
|
||
} finally {
|
||
addContainerLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 导入其他需要的组件
|
||
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);
|
||
}
|
||
|
||
/* 返回按钮样式 */
|
||
.back-btn {
|
||
font-size: 16px;
|
||
color: #409EFF;
|
||
padding: 8px 0;
|
||
margin-right: 16px;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
color: #66b1ff;
|
||
}
|
||
|
||
/* 服务器信息卡片 */
|
||
.server-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
/* 服务器详细信息卡片 */
|
||
.server-detail-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 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;
|
||
}
|
||
|
||
/* 硬件信息样式 - 符合整体设计风格 */
|
||
.device-count {
|
||
color: #909399;
|
||
font-size: 14px;
|
||
font-weight: normal;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.hardware-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.summary-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.hardware-devices {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.device-item {
|
||
padding: 16px;
|
||
background-color: #fafbfc;
|
||
border-radius: 6px;
|
||
border: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.device-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.device-title {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #409EFF;
|
||
}
|
||
|
||
.device-title .el-icon {
|
||
margin-right: 6px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.device-details {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.detail-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.usage-progress {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* 使用率状态颜色类 */
|
||
.info-value.usage-normal {
|
||
color: #67C23A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-value.usage-medium {
|
||
color: #409EFF;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-value.usage-warning {
|
||
color: #E6A23C;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-value.usage-critical {
|
||
color: #F56C6C;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 实际硬件划分样式 */
|
||
.highlight-item {
|
||
padding: 8px;
|
||
background-color: #f0f9ff;
|
||
border-radius: 4px;
|
||
border-left: 3px solid #409EFF;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 流量信息样式 */
|
||
.traffic-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.traffic-section:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
|
||
margin-bottom: 12px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.traffic-speed-grid,
|
||
.traffic-total-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.traffic-total-grid {
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
}
|
||
|
||
.traffic-distribution {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.raw-data {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background-color: #fafbfc;
|
||
border-radius: 6px;
|
||
border: 1px solid #e4e7ed;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.raw-data .info-value {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.traffic-note {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* 旧样式保持兼容 */
|
||
.usage-high {
|
||
color: #F56C6C;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.usage-medium {
|
||
color: #E6A23C;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 错误状态样式 */
|
||
.error-status {
|
||
color: #F56C6C;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 主要内容区域 */
|
||
.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;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.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;
|
||
|
||
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;
|
||
}
|
||
|
||
/* 服务器状态信息样式 */
|
||
.server-status-info {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.status-details {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.status-details p {
|
||
margin: 8px 0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.status-details strong {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status-warning {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #E6A23C;
|
||
font-weight: 500;
|
||
margin-top: 12px;
|
||
padding: 8px 12px;
|
||
background-color: rgba(230, 162, 60, 0.1);
|
||
border-radius: 4px;
|
||
border-left: 3px solid #E6A23C;
|
||
}
|
||
|
||
.status-warning .el-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 价格计算显示样式 */
|
||
.price-calculation {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.price-calculation .text-muted {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media screen and (max-width: 1200px) {
|
||
.server-info {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.info-card.location-info {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.server-detail-info {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 768px) {
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-header .left {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.back-btn {
|
||
margin-right: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.server-info {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.server-detail-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;
|
||
}
|
||
|
||
/* 硬件信息响应式 */
|
||
.hardware-summary {
|
||
grid-template-columns: 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.detail-row {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.device-item {
|
||
padding: 12px;
|
||
}
|
||
|
||
.device-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 流量信息响应式 */
|
||
.traffic-speed-grid,
|
||
.traffic-total-grid,
|
||
.traffic-distribution {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.raw-data {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
padding: 8px;
|
||
}
|
||
}
|
||
</style> |