This commit is contained in:
2025-07-15 18:02:29 +08:00
parent 2038ddc617
commit d636050aac
65 changed files with 17885 additions and 103 deletions
File diff suppressed because it is too large Load Diff
+314
View File
@@ -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