ACS
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="vm-list-container">
|
||||
<div class="filter-section">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
placeholder="输入关键字搜索"
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="vmList"
|
||||
style="width: 100%"
|
||||
border
|
||||
>
|
||||
<el-table-column prop="instance_id" label="ID" width="100" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.created_at }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.become_time }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.state)">
|
||||
{{ getStatusText(scope.row.state) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleManage(scope.row)"
|
||||
>
|
||||
<el-icon><menu /></el-icon>管理
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="handleStart(scope.row)"
|
||||
:disabled="scope.row.state === 'running'"
|
||||
>
|
||||
<el-icon><video-play /></el-icon>启动
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
@click="handleStop(scope.row)"
|
||||
:disabled="scope.row.state === 'stopped'"
|
||||
>
|
||||
<el-icon><video-pause /></el-icon>停止
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
link
|
||||
size="small"
|
||||
@click="handleRestart(scope.row)"
|
||||
:disabled="scope.row.state !== 'running'"
|
||||
>
|
||||
<el-icon><refresh-right /></el-icon>重启
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineProps, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
Search, Refresh, Menu, VideoPlay, VideoPause, RefreshRight
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
getContainer,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
restartInstance
|
||||
} from '@/utils/acs/server';
|
||||
|
||||
const props = defineProps({
|
||||
ID: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const vmList = ref([]);
|
||||
const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const searchKey = ref('');
|
||||
|
||||
// 获取虚拟机列表
|
||||
const fetchVmList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getContainer({
|
||||
server_id: props.ID,
|
||||
page: currentPage.value,
|
||||
count: pageSize.value,
|
||||
key: searchKey.value,
|
||||
user_id: ''
|
||||
});
|
||||
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
vmList.value = response.data.data || [];
|
||||
total.value = response.data.count || 0;
|
||||
} else {
|
||||
ElMessage.error('获取虚拟机列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取虚拟机列表出错:', error);
|
||||
ElMessage.error('获取虚拟机列表出错');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (state) => {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return 'success';
|
||||
case 'stopped':
|
||||
return 'danger';
|
||||
case 'paused':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (state) => {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return '运行中';
|
||||
case 'stopped':
|
||||
return '已停止';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'creating':
|
||||
return '创建中';
|
||||
case 'error':
|
||||
return '错误';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1;
|
||||
fetchVmList();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchKey.value = '';
|
||||
currentPage.value = 1;
|
||||
fetchVmList();
|
||||
};
|
||||
|
||||
// 处理页码变化
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val;
|
||||
fetchVmList();
|
||||
};
|
||||
|
||||
// 处理每页条数变化
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1;
|
||||
fetchVmList();
|
||||
};
|
||||
|
||||
// 管理虚拟机
|
||||
const handleManage = (row) => {
|
||||
router.push(`/servers/vm?instance_id=${row.instance_id}`);
|
||||
};
|
||||
|
||||
// 启动虚拟机
|
||||
const handleStart = async (row) => {
|
||||
try {
|
||||
const res = await startInstance(row.instance_id);
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
ElMessage.success('启动指令已发送');
|
||||
setTimeout(() => {
|
||||
fetchVmList();
|
||||
}, 2000);
|
||||
} else {
|
||||
ElMessage.error('启动失败: ' + (res.data.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动虚拟机出错:', error);
|
||||
ElMessage.error('启动虚拟机出错');
|
||||
}
|
||||
};
|
||||
|
||||
// 停止虚拟机
|
||||
const handleStop = async (row) => {
|
||||
try {
|
||||
ElMessageBox.confirm('确定要停止该虚拟机吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await stopInstance(row.instance_id);
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
ElMessage.success('停止指令已发送');
|
||||
setTimeout(() => {
|
||||
fetchVmList();
|
||||
}, 2000);
|
||||
} else {
|
||||
ElMessage.error('停止失败: ' + (res.data.message || '未知错误'));
|
||||
}
|
||||
}).catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('停止虚拟机出错:', error);
|
||||
ElMessage.error('停止虚拟机出错');
|
||||
}
|
||||
};
|
||||
|
||||
// 重启虚拟机
|
||||
const handleRestart = async (row) => {
|
||||
try {
|
||||
ElMessageBox.confirm('确定要重启该虚拟机吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await restartInstance(row.instance_id);
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
ElMessage.success('重启指令已发送');
|
||||
setTimeout(() => {
|
||||
fetchVmList();
|
||||
}, 2000);
|
||||
} else {
|
||||
ElMessage.error('重启失败: ' + (res.data.message || '未知错误'));
|
||||
}
|
||||
}).catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('重启虚拟机出错:', error);
|
||||
ElMessage.error('重启虚拟机出错');
|
||||
}
|
||||
};
|
||||
|
||||
// 监听ID变化
|
||||
watch(() => props.ID, (newVal) => {
|
||||
if (newVal) {
|
||||
fetchVmList();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.ID) {
|
||||
fetchVmList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vm-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>CPU使用率</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="cpuChart" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>内存使用率</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="memoryChart" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>磁盘使用率</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="diskChart" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>网络流量</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="networkChart" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, defineProps } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import * as echarts from 'echarts';
|
||||
import { getServerStatus, getTraffic, getDiskInfo } from '@/utils/acs/server';
|
||||
|
||||
const props = defineProps({
|
||||
Type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const cpuChart = ref(null);
|
||||
const memoryChart = ref(null);
|
||||
const diskChart = ref(null);
|
||||
const networkChart = ref(null);
|
||||
|
||||
let cpuChartInstance = null;
|
||||
let memoryChartInstance = null;
|
||||
let diskChartInstance = null;
|
||||
let networkChartInstance = null;
|
||||
|
||||
// 定时器ID
|
||||
let timer = null;
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 初始化CPU图表
|
||||
cpuChartInstance = echarts.init(cpuChart.value);
|
||||
const cpuOption = {
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c}%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'CPU',
|
||||
type: 'gauge',
|
||||
detail: { formatter: '{value}%' },
|
||||
data: [{ value: 0, name: '使用率' }],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 30,
|
||||
color: [
|
||||
[0.3, '#67C23A'],
|
||||
[0.7, '#E6A23C'],
|
||||
[1, '#F56C6C']
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
cpuChartInstance.setOption(cpuOption);
|
||||
|
||||
// 初始化内存图表
|
||||
memoryChartInstance = echarts.init(memoryChart.value);
|
||||
const memoryOption = {
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c}%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '内存',
|
||||
type: 'gauge',
|
||||
detail: { formatter: '{value}%' },
|
||||
data: [{ value: 0, name: '使用率' }],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 30,
|
||||
color: [
|
||||
[0.3, '#67C23A'],
|
||||
[0.7, '#E6A23C'],
|
||||
[1, '#F56C6C']
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
memoryChartInstance.setOption(memoryOption);
|
||||
|
||||
// 初始化磁盘图表
|
||||
diskChartInstance = echarts.init(diskChart.value);
|
||||
const diskOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: ['已使用', '可用']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '磁盘空间',
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['50%', '60%'],
|
||||
data: [
|
||||
{ value: 0, name: '已使用' },
|
||||
{ value: 100, name: '可用' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
diskChartInstance.setOption(diskOption);
|
||||
|
||||
// 初始化网络流量图表
|
||||
networkChartInstance = echarts.init(networkChart.value);
|
||||
const networkOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['上传', '下载']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array(10).fill('').map((_, i) => `${i}`)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} MB/s'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '上传',
|
||||
type: 'line',
|
||||
data: Array(10).fill(0),
|
||||
areaStyle: {}
|
||||
},
|
||||
{
|
||||
name: '下载',
|
||||
type: 'line',
|
||||
data: Array(10).fill(0),
|
||||
areaStyle: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
networkChartInstance.setOption(networkOption);
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateCharts = async () => {
|
||||
try {
|
||||
// 获取服务器状态
|
||||
const statusRes = await getServerStatus(route.query.server_id);
|
||||
if (statusRes && statusRes.data && statusRes.data.code === 200) {
|
||||
const statusData = statusRes.data.data;
|
||||
|
||||
// 更新CPU图表
|
||||
if (cpuChartInstance) {
|
||||
const cpuUsage = statusData.cpu_usage || 0;
|
||||
cpuChartInstance.setOption({
|
||||
series: [
|
||||
{
|
||||
data: [{ value: parseFloat(cpuUsage).toFixed(2), name: '使用率' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 更新内存图表
|
||||
if (memoryChartInstance) {
|
||||
const memoryUsage = statusData.memory_usage || 0;
|
||||
memoryChartInstance.setOption({
|
||||
series: [
|
||||
{
|
||||
data: [{ value: parseFloat(memoryUsage).toFixed(2), name: '使用率' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取磁盘信息
|
||||
const diskRes = await getDiskInfo(route.query.server_id);
|
||||
if (diskRes && diskRes.data && diskRes.data.code === 200) {
|
||||
const diskData = diskRes.data.data;
|
||||
if (diskChartInstance && diskData) {
|
||||
const used = diskData.used || 0;
|
||||
const available = diskData.available || 100;
|
||||
diskChartInstance.setOption({
|
||||
series: [
|
||||
{
|
||||
data: [
|
||||
{ value: used, name: '已使用' },
|
||||
{ value: available, name: '可用' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络流量
|
||||
const trafficRes = await getTraffic(route.query.server_id);
|
||||
if (trafficRes && trafficRes.data && trafficRes.data.code === 200) {
|
||||
const trafficData = trafficRes.data.data;
|
||||
if (networkChartInstance && trafficData) {
|
||||
// 假设API返回的是最近的流量数据点
|
||||
const uploadData = trafficData.upload || Array(10).fill(0);
|
||||
const downloadData = trafficData.download || Array(10).fill(0);
|
||||
|
||||
networkChartInstance.setOption({
|
||||
series: [
|
||||
{
|
||||
data: uploadData.slice(-10)
|
||||
},
|
||||
{
|
||||
data: downloadData.slice(-10)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新图表数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 调整图表大小
|
||||
const resizeCharts = () => {
|
||||
cpuChartInstance && cpuChartInstance.resize();
|
||||
memoryChartInstance && memoryChartInstance.resize();
|
||||
diskChartInstance && diskChartInstance.resize();
|
||||
networkChartInstance && networkChartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化图表
|
||||
initCharts();
|
||||
|
||||
// 定时更新数据
|
||||
updateCharts();
|
||||
timer = setInterval(updateCharts, 30000); // 每30秒更新一次
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除定时器
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
|
||||
// 销毁图表实例
|
||||
cpuChartInstance && cpuChartInstance.dispose();
|
||||
memoryChartInstance && memoryChartInstance.dispose();
|
||||
diskChartInstance && diskChartInstance.dispose();
|
||||
networkChartInstance && networkChartInstance.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user