fix: 提交修改
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
<h2 class="instance-name">{{ detail.name || '-' }} <span class="instance-id">{{ detail.id }}</span></h2>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<!-- <el-button type="warning" plain @click="openTokenDialog"><el-icon><Key /></el-icon>创建注册令牌</el-button> -->
|
||||
<el-button type="primary" plain @click="handleEdit">编辑宿主机</el-button>
|
||||
<el-button type="danger" plain @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
@@ -135,51 +136,69 @@
|
||||
<el-tab-pane label="监控" name="monitor">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">实时指标</h3>
|
||||
<h3 class="section-title">监控指标</h3>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<el-tag v-if="pollingActive" type="success" size="small" effect="plain">自动刷新中</el-tag>
|
||||
<el-button size="small" :icon="Refresh" @click="loadMetrics" :loading="metricsLoading">刷新指标</el-button>
|
||||
<el-select v-model="historyTimeRange" size="small" style="width: 120px" @change="loadHistoricalMetrics">
|
||||
<el-option v-for="option in historyTimeOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
<el-button size="small" :icon="Refresh" @click="loadHistoricalMetrics" :loading="historicalMetricsLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="metricsData">
|
||||
<template v-if="latestMetrics">
|
||||
<div class="metric-summary-row">
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">CPU 使用率</div>
|
||||
<div class="metric-summary-value">{{ latestMetrics.cpu_usage?.toFixed(1) }}%</div>
|
||||
<div class="metric-summary-sub">{{ latestMetrics.cpu_count }} 核</div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">内存使用率</div>
|
||||
<div class="metric-summary-value">{{ latestMetrics.mem_percent?.toFixed(1) }}%</div>
|
||||
<div class="metric-summary-sub">{{ formatBytesRaw(latestMetrics.mem_used) }} / {{ formatBytesRaw(latestMetrics.mem_total) }}</div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">公网流量</div>
|
||||
<div class="metric-summary-value">↓{{ formatNetLabel(latestMetrics.inet_rx) }}</div>
|
||||
<div class="metric-summary-sub">↑{{ formatNetLabel(latestMetrics.inet_tx) }}</div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">内网流量</div>
|
||||
<div class="metric-summary-value">↓{{ formatBytesRaw(latestMetrics.net_rx) }}</div>
|
||||
<div class="metric-summary-sub">↑{{ formatBytesRaw(latestMetrics.net_tx) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="historicalMetricsData">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12" v-if="metricsData.cpu">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> CPU 使用率 {{ (metricsData.cpu.cpu_usage_percent ?? 0).toFixed(1) }}% ({{ metricsData.cpu.cpu_count ?? '-' }}核)</span></template>
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> CPU 使用率</span></template>
|
||||
<div ref="cpuChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="metricsData.memory">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Coin /></el-icon> 内存 {{ formatBytesRaw(metricsData.memory.used) }} / {{ formatBytesRaw(metricsData.memory.total) }} ({{ metricsData.memory.percent ?? 0 }}%)</span></template>
|
||||
<template #header><span class="metrics-title"><el-icon><Coin /></el-icon> 内存使用率</span></template>
|
||||
<div ref="memChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16" style="margin-top: 16px">
|
||||
<el-col :span="12" v-if="metricsData.disk">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Box /></el-icon> 磁盘</span></template>
|
||||
<div v-for="(info, path) in metricsData.disk" :key="path" class="disk-item">
|
||||
<div class="disk-path">{{ path }}</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="总计">{{ formatBytesRaw(info.total) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已用">{{ formatBytesRaw(info.used) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="空闲">{{ formatBytesRaw(info.free) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="使用率">{{ info.percent ?? '-' }}%</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #header><span class="metrics-title"><el-icon><Connection /></el-icon> 公网流量</span></template>
|
||||
<div ref="inetChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="metricsData.network || metricsData.internet_speed">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Connection /></el-icon> 网络</span></template>
|
||||
<template #header><span class="metrics-title"><el-icon><Connection /></el-icon> 内网流量</span></template>
|
||||
<div ref="netChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-empty v-else description="加载指标数据中..." />
|
||||
<el-empty v-else-if="!historicalMetricsLoading" description="加载监控数据中..." :image-size="80" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -287,45 +306,55 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 创建组网弹窗 -->
|
||||
<el-dialog v-model="nwCreateVisible" title="创建组网" width="480px" destroy-on-close>
|
||||
<el-dialog v-model="nwCreateVisible" title="创建组网" width="480px" destroy-on-close class="tk-dialog">
|
||||
<el-form ref="nwCreateFormRef" :model="nwCreateForm" :rules="nwCreateRules" label-width="100px">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwCreateForm.user_id ? `${nwCreateUserName} (ID: ${nwCreateForm.user_id})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwUserSelector = true">选择</el-button>
|
||||
<el-button v-if="nwCreateForm.user_id" @click="nwCreateForm.user_id = 0; nwCreateUserName = ''">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网桥名称">
|
||||
<el-input v-model="nwCreateForm.bridge_name" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关">
|
||||
<el-input v-model="nwCreateForm.gateway" placeholder="可选,如 10.0.0.1" />
|
||||
</el-form-item>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">组网信息</div>
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwCreateForm.user_id ? `${nwCreateUserName} (ID: ${nwCreateForm.user_id})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwUserSelector = true">选择</el-button>
|
||||
<el-button v-if="nwCreateForm.user_id" @click="nwCreateForm.user_id = 0; nwCreateUserName = ''">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网桥名称">
|
||||
<el-input v-model="nwCreateForm.bridge_name" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关">
|
||||
<el-input v-model="nwCreateForm.gateway" placeholder="可选,如 10.0.0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nwCreateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwCreate">创建</el-button>
|
||||
<div class="tk-dialog-footer">
|
||||
<el-button @click="nwCreateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwCreate">创建</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配IP弹窗 -->
|
||||
<el-dialog v-model="nwAssignVisible" title="为虚拟机分配组网IP" width="480px" destroy-on-close>
|
||||
<el-dialog v-model="nwAssignVisible" title="为虚拟机分配组网IP" width="480px" destroy-on-close class="tk-dialog">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="组网">{{ nwAssignTarget?.name || '-' }} (ID: {{ nwAssignTarget?.id }})</el-form-item>
|
||||
<el-form-item label="虚拟机" required>
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwAssignVmId ? `${nwAssignVmName} (ID: ${nwAssignVmId})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwVmSelector = true">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定IP">
|
||||
<el-input v-model="nwAssignIp" placeholder="留空自动分配" />
|
||||
</el-form-item>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">分配信息</div>
|
||||
<el-form-item label="组网">{{ nwAssignTarget?.name || '-' }} (ID: {{ nwAssignTarget?.id }})</el-form-item>
|
||||
<el-form-item label="虚拟机" required>
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="nwAssignVmId ? `${nwAssignVmName} (ID: ${nwAssignVmId})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showNwVmSelector = true">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定IP">
|
||||
<el-input v-model="nwAssignIp" placeholder="留空自动分配" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nwAssignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwAssign" :disabled="!nwAssignVmId">分配</el-button>
|
||||
<div class="tk-dialog-footer">
|
||||
<el-button @click="nwAssignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwAssign" :disabled="!nwAssignVmId">分配</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -334,61 +363,163 @@
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑宿主机" width="890px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="formData.name" /></el-form-item>
|
||||
<el-form-item label="服务地址" prop="base_url"><el-input v-model="formData.base_url" /></el-form-item>
|
||||
<el-form-item label="IP 地址" prop="ip"><el-input v-model="formData.ip" /></el-form-item>
|
||||
<el-form-item label="认证Token"><el-input v-model="formData.token" show-password /></el-form-item>
|
||||
<el-divider content-position="left">SSH 配置</el-divider>
|
||||
<el-form-item label="SSH 端口"><el-input-number v-model="formData.port" :min="0" :max="65535" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="SSH 用户名"><el-input v-model="formData.user" /></el-form-item>
|
||||
<el-form-item label="SSH 密码"><el-input v-model="formData.password" show-password /></el-form-item>
|
||||
<el-form-item label="私钥"><el-input v-model="formData.private_key" type="textarea" :rows="4" placeholder="SSH 私钥内容" /></el-form-item>
|
||||
<el-divider content-position="left">资源限制</el-divider>
|
||||
<el-form-item label="最大CPU(核)"><el-input-number v-model="formData.max_cpu" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大内存">
|
||||
<div class="unit-input-row">
|
||||
<el-select v-model="memoryUnit" style="width: 70px; flex-shrink: 0;" size="default">
|
||||
<el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
<el-input-number v-model="memoryDisplay" :min="0" controls-position="right" class="wide-number" />
|
||||
</div>
|
||||
<el-dialog v-model="editDialogVisible" title="编辑宿主机" width="890px" destroy-on-close class="tk-dialog">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">基本信息</div>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="formData.name" /></el-form-item>
|
||||
<el-form-item label="服务地址" prop="base_url"><el-input v-model="formData.base_url" /></el-form-item>
|
||||
<el-form-item label="IP 地址" prop="ip"><el-input v-model="formData.ip" /></el-form-item>
|
||||
<el-form-item label="认证Token"><el-input v-model="formData.token" show-password /></el-form-item>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">SSH 配置</div>
|
||||
<el-form-item label="端口"><el-input-number v-model="formData.port" :min="0" :max="65535" controls-position="right" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="用户名"><el-input v-model="formData.user" /></el-form-item>
|
||||
<el-form-item label="密码"><el-input v-model="formData.password" show-password /></el-form-item>
|
||||
<el-form-item label="私钥"><el-input v-model="formData.private_key" type="textarea" :rows="4" placeholder="SSH 私钥内容" /></el-form-item>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">资源限制</div>
|
||||
<div class="tk-resource-grid">
|
||||
<el-form-item label="CPU"><el-input-number v-model="formData.max_cpu" :min="0" controls-position="right" /><span class="tk-res-unit">核</span></el-form-item>
|
||||
<el-form-item label="内存">
|
||||
<el-input-number v-model="memoryDisplay" :min="0" controls-position="right" />
|
||||
<el-select v-model="memoryUnit" class="tk-unit-select">
|
||||
<el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大磁盘">
|
||||
<div class="unit-input-row">
|
||||
<el-select v-model="diskUnit" style="width: 70px; flex-shrink: 0;" size="default">
|
||||
<el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
<el-input-number v-model="diskDisplay" :min="0" controls-position="right" class="wide-number" />
|
||||
</div>
|
||||
<el-form-item label="磁盘">
|
||||
<el-input-number v-model="diskDisplay" :min="0" controls-position="right" />
|
||||
<el-select v-model="diskUnit" class="tk-unit-select">
|
||||
<el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="下行带宽(Mbps)"><el-input-number v-model="formData.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="上行带宽(Mbps)"><el-input-number v-model="formData.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="宿主机组">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="formData.host_group_id ? `宿主机组 #${formData.host_group_id}` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showGroupSelector = true">选择</el-button>
|
||||
<el-button v-if="formData.host_group_id" @click="formData.host_group_id = 0">清除</el-button>
|
||||
<el-form-item label="下行带宽"><el-input-number v-model="formData.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span></el-form-item>
|
||||
<el-form-item label="上行带宽"><el-input-number v-model="formData.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span></el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="介绍"><el-input v-model="formData.description" type="textarea" :rows="3" /></el-form-item>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">其他配置</div>
|
||||
<el-form-item label="宿主机组">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="formData.host_group_id ? `宿主机组 #${formData.host_group_id}` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showGroupSelector = true">选择</el-button>
|
||||
<el-button v-if="formData.host_group_id" @click="formData.host_group_id = 0">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="介绍"><el-input v-model="formData.description" type="textarea" :rows="3" /></el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
<div class="tk-dialog-footer">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<HostGroupSelectorPopup v-model="showGroupSelector" :service-id="serviceId" :current-id="formData.host_group_id" @confirm="g => formData.host_group_id = g.id" />
|
||||
|
||||
<!-- 创建注册令牌弹窗 -->
|
||||
<el-dialog v-model="tokenDialogVisible" title="创建宿主机注册令牌" width="700px" destroy-on-close class="token-dialog">
|
||||
<el-form ref="tokenFormRef" :model="tokenForm" :rules="tokenRules" label-width="120px">
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">基本信息</div>
|
||||
<el-form-item label="宿主机名称" prop="name">
|
||||
<el-input v-model="tokenForm.name" placeholder="为该宿主机命名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属宿主机组" prop="host_group_id">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input :model-value="tokenForm.host_group_id ? `宿主机组 #${tokenForm.host_group_id}` : ''" placeholder="请选择宿主机组" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showTokenGroupSelector = true">选择</el-button>
|
||||
<el-button v-if="tokenForm.host_group_id" @click="tokenForm.host_group_id = 0">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="宿主机描述">
|
||||
<el-input v-model="tokenForm.description" type="textarea" :rows="2" placeholder="宿主机描述(可选)" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">资源配额</div>
|
||||
<div class="tk-resource-grid">
|
||||
<el-form-item label="CPU" prop="max_cpu" class="tk-res-item">
|
||||
<el-input-number v-model="tokenForm.max_cpu" :min="1" controls-position="right" /><span class="tk-res-unit">核</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="内存" prop="max_memory" class="tk-res-item">
|
||||
<el-input-number v-model="tokenMemDisplay" :min="0" controls-position="right" />
|
||||
<el-select v-model="tokenMemUnit" class="tk-unit-select">
|
||||
<el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="磁盘" prop="max_disk" class="tk-res-item">
|
||||
<el-input-number v-model="tokenDiskDisplay" :min="0" controls-position="right" />
|
||||
<el-select v-model="tokenDiskUnit" class="tk-unit-select">
|
||||
<el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="下行带宽" class="tk-res-item">
|
||||
<el-input-number v-model="tokenForm.rx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="上行带宽" class="tk-res-item">
|
||||
<el-input-number v-model="tokenForm.tx_bandwidth" :min="0" controls-position="right" /><span class="tk-res-unit">Mbps</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">令牌有效期</div>
|
||||
<el-form-item label="有效期" prop="expire_hours">
|
||||
<el-input-number v-model="tokenForm.expire_hours" :min="1" :max="8760" controls-position="right" style="width: 100%" />
|
||||
<div class="form-hint">单位:小时。默认 24 小时,最大 8760 小时(365天)</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="tk-dialog-footer">
|
||||
<el-button @click="tokenDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="tokenSubmitLoading" @click="handleTokenSubmit">
|
||||
<el-icon><Key /></el-icon>创建令牌
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 令牌结果弹窗 -->
|
||||
<el-dialog v-model="tokenResultVisible" title="注册令牌已生成" width="560px" :close-on-click-modal="false" class="token-result-dialog">
|
||||
<div class="tk-result-wrapper">
|
||||
<div class="tk-result-header">
|
||||
<el-icon class="tk-result-icon"><Key /></el-icon>
|
||||
<div>
|
||||
<div class="tk-result-name">{{ tokenResultInfo.name }}</div>
|
||||
<div class="tk-result-meta">有效期 {{ tokenResultInfo.expire_hours }} 小时</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert type="warning" :closable="false" show-icon style="margin-bottom: 16px">
|
||||
<template #title>请立即复制并保存此令牌,关闭后将无法再次查看</template>
|
||||
</el-alert>
|
||||
<div class="tk-token-block">
|
||||
<div class="tk-token-label">后端地址</div>
|
||||
<div class="tk-token-value">{{ baseUrl }}</div>
|
||||
</div>
|
||||
<div class="tk-token-block">
|
||||
<div class="tk-token-label">service_id(主控服务ID)</div>
|
||||
<div class="tk-token-value">{{ tokenResultInfo.service_id }}</div>
|
||||
</div>
|
||||
<div class="tk-token-block">
|
||||
<div class="tk-token-label">注册令牌</div>
|
||||
<div class="tk-token-value">{{ tokenResultInfo.token }}</div>
|
||||
</div>
|
||||
<el-button type="primary" class="tk-copy-btn" @click="copyToken">
|
||||
<el-icon><CopyDocument /></el-icon>复制令牌到剪贴板
|
||||
</el-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="tokenResultVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 令牌用宿主机组选择器 -->
|
||||
<HostGroupSelectorPopup v-model="showTokenGroupSelector" :service-id="serviceId" :current-id="tokenForm.host_group_id" @confirm="handleTokenGroupSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -396,13 +527,15 @@
|
||||
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Box, Connection, Search, Plus } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Connection, Search, Plus, Key, CopyDocument } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost,
|
||||
getRemoteHostDetail, updateRemoteHost, deleteRemoteHost,
|
||||
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
|
||||
assignUserNetworking, removeUserNetworkingNetwork
|
||||
assignUserNetworking, removeUserNetworkingNetwork,
|
||||
createHostToken, getMetricsHistory
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import { baseUrl } from '@/config/env'
|
||||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||||
import ImageManage from '@/views/virtualization/ImageManage.vue'
|
||||
import NetworkManage from '@/views/virtualization/NetworkManage.vue'
|
||||
@@ -442,14 +575,16 @@ watch(activeTab, (tab) => {
|
||||
nextTick(() => { tabRefMap[tab]?.value?.loadList?.() })
|
||||
}
|
||||
}
|
||||
if (tab === 'monitor' && detail.value) { loadMetrics(); startPolling() }
|
||||
else stopPolling()
|
||||
if (tab === 'monitor' && detail.value) {
|
||||
if (!historicalMetricsData.value) {
|
||||
loadHistoricalMetrics()
|
||||
}
|
||||
}
|
||||
if (tab === 'networking') loadNetworkingList()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const metricsLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
provide('embedded', true)
|
||||
@@ -480,7 +615,8 @@ const fallbackCopy = (text) => {
|
||||
} catch { ElMessage.error('复制失败') }
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
const metricsData = ref(null)
|
||||
const historicalMetricsData = ref(null)
|
||||
const historicalMetricsLoading = ref(false)
|
||||
const editDialogVisible = ref(false)
|
||||
const showGroupSelector = ref(false)
|
||||
const formRef = ref(null)
|
||||
@@ -561,65 +697,77 @@ const loadDetail = async () => {
|
||||
const cpuChartRef = ref(null)
|
||||
const memChartRef = ref(null)
|
||||
const netChartRef = ref(null)
|
||||
const inetChartRef = ref(null)
|
||||
let cpuChart = null
|
||||
let memChart = null
|
||||
let netChart = null
|
||||
|
||||
const MAX_HISTORY = 60
|
||||
const metricsHistory = reactive({
|
||||
times: [],
|
||||
cpu: [],
|
||||
memPercent: [],
|
||||
netRx: [],
|
||||
netTx: []
|
||||
})
|
||||
const pollingActive = ref(false)
|
||||
let pollTimer = null
|
||||
let inetChart = null
|
||||
let isPageActive = false
|
||||
|
||||
const loadMetrics = async () => {
|
||||
if (!serviceId.value || !hostId.value || !isPageActive) return
|
||||
metricsLoading.value = true
|
||||
const latestMetrics = computed(() => {
|
||||
const arr = historicalMetricsData.value
|
||||
if (!Array.isArray(arr) || !arr.length) return null
|
||||
return arr[arr.length - 1]
|
||||
})
|
||||
|
||||
// 历史指标时间范围
|
||||
const historyTimeRange = ref('1m') // 1m, 5m, 1h, 1d
|
||||
const historyTimeOptions = [
|
||||
{ label: '最近1分钟', value: '1m' },
|
||||
{ label: '最近5分钟', value: '5m' },
|
||||
{ label: '最近1小时', value: '1h' },
|
||||
{ label: '最近1天', value: '1d' },
|
||||
]
|
||||
|
||||
// 加载历史指标数据
|
||||
const loadHistoricalMetrics = async () => {
|
||||
if (!serviceId.value || !hostId.value) return
|
||||
|
||||
historicalMetricsLoading.value = true
|
||||
try {
|
||||
const res = await getRemoteHostMetrics({ service_id: serviceId.value, host_id: hostId.value })
|
||||
// 计算时间范围
|
||||
const now = new Date()
|
||||
let startTime = new Date()
|
||||
|
||||
switch (historyTimeRange.value) {
|
||||
case '1m':
|
||||
startTime.setMinutes(now.getMinutes() - 1)
|
||||
break
|
||||
case '5m':
|
||||
startTime.setMinutes(now.getMinutes() - 5)
|
||||
break
|
||||
case '1h':
|
||||
startTime.setHours(now.getHours() - 1)
|
||||
break
|
||||
case '1d':
|
||||
startTime.setDate(now.getDate() - 1)
|
||||
break
|
||||
}
|
||||
|
||||
const params = {
|
||||
service_id: serviceId.value,
|
||||
host_id: hostId.value,
|
||||
start: startTime.toISOString(),
|
||||
end_time: now.toISOString(),
|
||||
interval: { '1m': '1m', '5m': '5m', '1h': '1h', '1d': '1d' }[historyTimeRange.value] || '5m'
|
||||
}
|
||||
|
||||
const res = await getMetricsHistory(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
metricsData.value = body.data.data ?? body.data
|
||||
pushHistory(metricsData.value)
|
||||
historicalMetricsData.value = Array.isArray(body.data) ? body.data : (body.data.data || [])
|
||||
await nextTick()
|
||||
renderCharts()
|
||||
renderHistoricalCharts()
|
||||
} else {
|
||||
ElMessage.error(extractApiError(body, '加载历史指标失败'))
|
||||
}
|
||||
} catch { /* silent for polling */ } finally { metricsLoading.value = false }
|
||||
}
|
||||
|
||||
const pushHistory = (d) => {
|
||||
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
metricsHistory.times.push(now)
|
||||
metricsHistory.cpu.push(d.cpu?.cpu_usage_percent ?? 0)
|
||||
metricsHistory.memPercent.push(d.memory?.percent ?? 0)
|
||||
metricsHistory.netRx.push(d.internet_speed?.rx_bytes ?? 0)
|
||||
metricsHistory.netTx.push(d.internet_speed?.tx_bytes ?? 0)
|
||||
if (metricsHistory.times.length > MAX_HISTORY) {
|
||||
metricsHistory.times.shift()
|
||||
metricsHistory.cpu.shift()
|
||||
metricsHistory.memPercent.shift()
|
||||
metricsHistory.netRx.shift()
|
||||
metricsHistory.netTx.shift()
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '加载历史指标失败'))
|
||||
} finally {
|
||||
historicalMetricsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const makeLineOption = (title, seriesData, color, yFormatter) => ({
|
||||
tooltip: { trigger: 'axis', formatter: (params) => {
|
||||
const p = params[0]
|
||||
return `${p.axisValue}<br/>${p.marker} ${p.seriesName}: ${yFormatter ? yFormatter(p.value) : p.value}`
|
||||
}},
|
||||
grid: { top: 10, right: 16, bottom: 24, left: 50 },
|
||||
xAxis: { type: 'category', data: metricsHistory.times, boundaryGap: false, axisLabel: { fontSize: 10 } },
|
||||
yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: yFormatter || (v => v) } },
|
||||
series: Array.isArray(seriesData)
|
||||
? seriesData.map(s => ({ name: s.name, type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2 }, data: s.data, itemStyle: { color: s.color } }))
|
||||
: [{ name: title, type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color }, itemStyle: { color }, data: seriesData }]
|
||||
})
|
||||
|
||||
const formatNetLabel = (v) => {
|
||||
if (!v) return '0 B/s'
|
||||
@@ -629,33 +777,78 @@ const formatNetLabel = (v) => {
|
||||
return v + ' B/s'
|
||||
}
|
||||
|
||||
const renderCharts = () => {
|
||||
const times = [...metricsHistory.times]
|
||||
const cpuData = [...metricsHistory.cpu]
|
||||
const memData = [...metricsHistory.memPercent]
|
||||
const rxData = [...metricsHistory.netRx]
|
||||
const txData = [...metricsHistory.netTx]
|
||||
// 渲染历史指标图表
|
||||
const renderHistoricalCharts = () => {
|
||||
const metrics = historicalMetricsData.value
|
||||
if (!Array.isArray(metrics) || !metrics.length) return
|
||||
|
||||
const range = historyTimeRange.value
|
||||
const showDate = range === '7d' || range === '24h'
|
||||
const symbolType = range === '7d' ? 'circle' : 'none'
|
||||
const labelRotate = showDate ? 45 : 0
|
||||
|
||||
const times = metrics.map(m => {
|
||||
const date = new Date(m.bucket)
|
||||
if (range === '7d') return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
|
||||
})
|
||||
|
||||
const cpuData = metrics.map(m => m.cpu_usage ?? 0)
|
||||
const memData = metrics.map(m => m.mem_percent ?? 0)
|
||||
const inetRxData = metrics.map(m => m.inet_rx ?? 0)
|
||||
const inetTxData = metrics.map(m => m.inet_tx ?? 0)
|
||||
|
||||
const netRxRate = []
|
||||
const netTxRate = []
|
||||
for (let i = 0; i < metrics.length; i++) {
|
||||
if (i === 0) { netRxRate.push(0); netTxRate.push(0); continue }
|
||||
const dt = (new Date(metrics[i].bucket) - new Date(metrics[i - 1].bucket)) / 1000
|
||||
if (dt > 0) {
|
||||
netRxRate.push(Math.max(0, ((metrics[i].net_rx ?? 0) - (metrics[i - 1].net_rx ?? 0)) / dt))
|
||||
netTxRate.push(Math.max(0, ((metrics[i].net_tx ?? 0) - (metrics[i - 1].net_tx ?? 0)) / dt))
|
||||
} else {
|
||||
netRxRate.push(0); netTxRate.push(0)
|
||||
}
|
||||
}
|
||||
|
||||
const baseGrid = { top: 10, right: 16, bottom: 24, left: 50 }
|
||||
const makeXAxis = () => ({ type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10, rotate: labelRotate } })
|
||||
const makeSeries = (name, data, color) => ({ name, type: 'line', smooth: true, symbol: symbolType, areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color }, itemStyle: { color }, data })
|
||||
|
||||
if (cpuChartRef.value) {
|
||||
if (!cpuChart) cpuChart = echarts.init(cpuChartRef.value)
|
||||
cpuChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} CPU: ${p[0].value.toFixed(1)}%` },
|
||||
grid: { top: 10, right: 16, bottom: 24, left: 50 },
|
||||
xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10 } },
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
|
||||
series: [{ name: 'CPU', type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color: '#409eff' }, itemStyle: { color: '#409eff' }, data: cpuData }]
|
||||
series: [makeSeries('CPU', cpuData, '#409eff')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (memChartRef.value) {
|
||||
if (!memChart) memChart = echarts.init(memChartRef.value)
|
||||
memChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} 内存: ${p[0].value.toFixed(1)}%` },
|
||||
grid: { top: 10, right: 16, bottom: 24, left: 50 },
|
||||
xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10 } },
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
|
||||
series: [{ name: '内存', type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color: '#67c23a' }, itemStyle: { color: '#67c23a' }, data: memData }]
|
||||
series: [makeSeries('内存', memData, '#67c23a')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (inetChartRef.value) {
|
||||
if (!inetChart) inetChart = echarts.init(inetChartRef.value)
|
||||
inetChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (params) => {
|
||||
let s = params[0].axisValue
|
||||
params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatNetLabel(p.value)}` })
|
||||
return s
|
||||
}},
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: formatNetLabel } },
|
||||
series: [makeSeries('接收', inetRxData, '#409eff'), makeSeries('发送', inetTxData, '#e6a23c')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (netChartRef.value) {
|
||||
if (!netChart) netChart = echarts.init(netChartRef.value)
|
||||
netChart.setOption({
|
||||
@@ -664,33 +857,18 @@ const renderCharts = () => {
|
||||
params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatNetLabel(p.value)}` })
|
||||
return s
|
||||
}},
|
||||
grid: { top: 10, right: 16, bottom: 24, left: 50 },
|
||||
xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10 } },
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: formatNetLabel } },
|
||||
series: [
|
||||
{ name: '接收', type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color: '#409eff' }, itemStyle: { color: '#409eff' }, data: rxData },
|
||||
{ name: '发送', type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color: '#e6a23c' }, itemStyle: { color: '#e6a23c' }, data: txData }
|
||||
]
|
||||
series: [makeSeries('接收', netRxRate, '#409eff'), makeSeries('发送', netTxRate, '#e6a23c')]
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
|
||||
const startPolling = () => {
|
||||
if (!serviceId.value || !hostId.value || !isPageActive) return
|
||||
stopPolling()
|
||||
pollingActive.value = true
|
||||
pollTimer = setInterval(() => { loadMetrics() }, 3000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
pollingActive.value = false
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
}
|
||||
|
||||
const disposeCharts = () => {
|
||||
cpuChart?.dispose(); cpuChart = null
|
||||
memChart?.dispose(); memChart = null
|
||||
netChart?.dispose(); netChart = null
|
||||
inetChart?.dispose(); inetChart = null
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
@@ -736,6 +914,115 @@ const handleDelete = () => {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// ========== 创建注册令牌 ==========
|
||||
const tokenDialogVisible = ref(false)
|
||||
const tokenSubmitLoading = ref(false)
|
||||
const tokenResultVisible = ref(false)
|
||||
const showTokenGroupSelector = ref(false)
|
||||
const tokenFormRef = ref(null)
|
||||
const tokenMemUnit = ref('GB')
|
||||
const tokenDiskUnit = ref('GB')
|
||||
|
||||
const tokenForm = reactive({
|
||||
name: '', host_group_id: 0, max_cpu: 4,
|
||||
max_memory: 4194304, max_disk: 100,
|
||||
rx_bandwidth: 100, tx_bandwidth: 100,
|
||||
description: '', expire_hours: 24
|
||||
})
|
||||
const tokenResultInfo = reactive({ name: '', expire_hours: 24, token: '', service_id: 0 })
|
||||
const tokenRules = {
|
||||
name: [{ required: true, message: '请输入宿主机名称', trigger: 'blur' }],
|
||||
host_group_id: [{ required: true, type: 'number', min: 1, message: '请选择宿主机组', trigger: 'change' }],
|
||||
max_cpu: [{ required: true, type: 'number', min: 1, message: '请设置最大CPU核数', trigger: 'change' }],
|
||||
max_memory: [{ required: true, type: 'number', min: 1, message: '请设置最大内存', trigger: 'change' }],
|
||||
max_disk: [{ required: true, type: 'number', min: 1, message: '请设置最大磁盘', trigger: 'change' }],
|
||||
expire_hours: [{ required: true, type: 'number', min: 1, message: '请设置有效期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const getTokenMemFactor = () => memoryUnitOptions.find(u => u.label === tokenMemUnit.value)?.factor || 1048576
|
||||
const getTokenDiskFactor = () => diskUnitOptions.find(u => u.label === tokenDiskUnit.value)?.factor || 1
|
||||
const tokenMemDisplay = computed({
|
||||
get: () => tokenForm.max_memory ? +(tokenForm.max_memory / getTokenMemFactor()).toFixed(2) : 0,
|
||||
set: (v) => { tokenForm.max_memory = Math.round((v || 0) * getTokenMemFactor()) }
|
||||
})
|
||||
const tokenDiskDisplay = computed({
|
||||
get: () => tokenForm.max_disk ? +(tokenForm.max_disk / getTokenDiskFactor()).toFixed(2) : 0,
|
||||
set: (v) => { tokenForm.max_disk = Math.round((v || 0) * getTokenDiskFactor()) }
|
||||
})
|
||||
|
||||
const openTokenDialog = () => {
|
||||
const d = detail.value
|
||||
Object.assign(tokenForm, {
|
||||
name: '', host_group_id: d?.host_group_id || 0,
|
||||
max_cpu: d?.max_cpu || 4,
|
||||
max_memory: d?.max_memory || 4194304,
|
||||
max_disk: d?.max_disk || 100,
|
||||
rx_bandwidth: d?.rx_bandwidth || 100,
|
||||
tx_bandwidth: d?.tx_bandwidth || 100,
|
||||
description: '', expire_hours: 24
|
||||
})
|
||||
tokenMemUnit.value = 'GB'
|
||||
tokenDiskUnit.value = 'GB'
|
||||
tokenDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleTokenGroupSelected = (group) => {
|
||||
tokenForm.host_group_id = group.id
|
||||
}
|
||||
|
||||
const handleTokenSubmit = () => {
|
||||
tokenFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
tokenSubmitLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('name', tokenForm.name)
|
||||
fd.append('host_group_id', tokenForm.host_group_id)
|
||||
fd.append('max_cpu', tokenForm.max_cpu)
|
||||
fd.append('max_memory', tokenForm.max_memory)
|
||||
fd.append('max_disk', tokenForm.max_disk)
|
||||
fd.append('rx_bandwidth', tokenForm.rx_bandwidth)
|
||||
fd.append('tx_bandwidth', tokenForm.tx_bandwidth)
|
||||
fd.append('description', tokenForm.description || '')
|
||||
fd.append('expire_hours', tokenForm.expire_hours)
|
||||
const res = await createHostToken(fd)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
tokenResultInfo.name = tokenForm.name
|
||||
tokenResultInfo.expire_hours = tokenForm.expire_hours
|
||||
tokenResultInfo.token = body.data.token || body.data.Token || JSON.stringify(body.data)
|
||||
tokenResultInfo.service_id = serviceId.value
|
||||
tokenDialogVisible.value = false
|
||||
tokenResultVisible.value = true
|
||||
ElMessage.success('注册令牌创建成功')
|
||||
} else {
|
||||
ElMessage.error(extractApiError(body, '创建令牌失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '创建令牌失败'))
|
||||
} finally {
|
||||
tokenSubmitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const copyToken = async () => {
|
||||
const text = `后端地址:${baseUrl}\nservice_id:${tokenResultInfo.service_id}\n注册令牌:${tokenResultInfo.token}`
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('令牌信息已复制到剪贴板')
|
||||
} catch {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
ElMessage.success('令牌信息已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
tagsViewStore.delVisitedView(route)
|
||||
router.push({ path: '/virtualization/kvm-service-detail', query: { service_id: serviceId.value, service_name: serviceName.value } })
|
||||
@@ -916,26 +1203,20 @@ const initPage = () => {
|
||||
showToken.value = false
|
||||
showPassword.value = false
|
||||
showPrivateKey.value = false
|
||||
metricsData.value = null
|
||||
metricsHistory.times.length = 0
|
||||
metricsHistory.cpu.length = 0
|
||||
metricsHistory.memPercent.length = 0
|
||||
metricsHistory.netRx.length = 0
|
||||
metricsHistory.netTx.length = 0
|
||||
historicalMetricsData.value = null
|
||||
disposeCharts()
|
||||
loadDetail()
|
||||
if (activeTab.value === 'monitor') loadMetrics().then(() => startPolling())
|
||||
if (activeTab.value === 'monitor') loadHistoricalMetrics()
|
||||
}
|
||||
|
||||
watch(hostId, () => { if (isPageActive) initPage() })
|
||||
onActivated(() => {
|
||||
isPageActive = true
|
||||
if (loadedHostId !== hostId.value) initPage()
|
||||
else if (activeTab.value === 'monitor') startPolling()
|
||||
})
|
||||
onMounted(() => { isPageActive = true; initPage() })
|
||||
onDeactivated(() => { isPageActive = false; stopPolling() })
|
||||
onBeforeUnmount(() => { isPageActive = false; stopPolling(); disposeCharts() })
|
||||
onDeactivated(() => { isPageActive = false })
|
||||
onBeforeUnmount(() => { isPageActive = false; disposeCharts() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -981,16 +1262,16 @@ onBeforeUnmount(() => { isPageActive = false; stopPolling(); disposeCharts() })
|
||||
.secret-cell { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||||
.secret-cell code { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mono-text { font-family: 'Consolas', 'Monaco', monospace; }
|
||||
.text-muted { color: #c0c4cc; }
|
||||
|
||||
.metrics-card { margin-bottom: 0; }
|
||||
.metrics-title { font-weight: 600; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.metrics-title .el-icon { font-size: 16px; color: #409eff; }
|
||||
.chart-container { width: 100%; height: 220px; }
|
||||
.disk-item { margin-bottom: 8px; }
|
||||
.disk-path { font-weight: 500; color: #409eff; font-size: 13px; margin-bottom: 4px; font-family: 'Consolas', monospace; }
|
||||
|
||||
.unit-input-row { display: flex; gap: 6px; width: 100%; }
|
||||
.wide-number { flex: 1; min-width: 140px; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
.metric-summary-row { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.metric-summary-card { flex: 1; min-width: 0; background: #f7f8fa; border-radius: 6px; padding: 14px 16px; border: 1px solid #e8e8e8; display: flex; flex-direction: column; }
|
||||
.metric-summary-label { font-size: 12px; color: #86909c; margin-bottom: 8px; }
|
||||
.metric-summary-value { font-size: 22px; font-weight: 600; color: #1d2129; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.metric-summary-sub { font-size: 12px; color: #86909c; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user