Files
ApiServer-Web-admin_dashboa…/src/views/acs/nodes/server.vue
T
lin 11cb40c86a
Build and Deploy Vue3 / build (push) Successful in 2m57s
Build and Deploy Vue3 / deploy (push) Successful in 2m48s
fix:修改实例规格参数和镜像管理参数
2025-10-24 17:34:52 +08:00

3198 lines
99 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>