feat:对接虚拟机实例接口

This commit is contained in:
2025-08-14 11:31:13 +08:00
parent d636050aac
commit 67ce6f66ac
9 changed files with 4941 additions and 105 deletions
+2131
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -192,6 +192,16 @@ const routes = [
title: '服务器详情',
hidden: true
}
},
// 虚拟机详情页面路由
{
path: 'servers/vm',
name: 'VmDetail',
component: () => import('../views/acs/nodes/VmDetail.vue'),
meta: {
title: '虚拟机详情',
hidden: true
}
}
]
},
+16
View File
@@ -83,6 +83,13 @@ export const getContainer = data => {
`/v1/admin/container/get_container_list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
);
};
/**获取虚拟机列表 */
export const getInstanceList = data => {
return http2.get(
`/v1/admin/instance/list?server_id=${data.server_id}&user_id=${data.user_id}&page=${data.page}&count=${data.count}&key=${data.key}`
);
};
/**获取单个指定容器 */
export const getOneContainer = data => {
return http2.post("/v1/admin/container/get_container_detail", data, {
@@ -453,3 +460,12 @@ export const changeInstancePasswordUser = (id, data) => {
}
});
};
/**删除端口 */
export const deletePort = data => {
return http2.post("/v1/admin/instance_port/delete", data, {
headers: {
"Content-Type": "multipart/form-data"
}
});
};
+3 -2
View File
@@ -4,6 +4,7 @@ import router from '@/router'
// 基础URL
const baseUrl = 'https://apiservertest.s1f.ren'
// const baseUrl = 'https://cloudapi.007yjs.com'
// 检查URL是否需要认证
const urlNeedAuth = (url) => {
@@ -105,7 +106,7 @@ class Request {
// 创建默认实例
const request = new Request({
baseURL: baseUrl,
timeout: 5000,
timeout: 30000,
headers: {
'Content-Type': 'multipart/form-data'
}
@@ -115,7 +116,7 @@ export const mainUrl = baseUrl + "/acs"
export const http2 = axios.create({
baseURL: baseUrl,
timeout: 5000,
timeout: 30000,
headers: {},
});
-1
View File
@@ -901,4 +901,3 @@ onMounted(() => {
}
}
</style>
</style>
+47 -31
View File
@@ -181,7 +181,7 @@
</el-form-item>
<el-form-item label="内存">
<el-input v-model="serverForm.memory" placeholder="内存大小">
<template #append>GB</template>
<template #append>MB</template>
</el-input>
</el-form-item>
<el-form-item label="CPU">
@@ -712,40 +712,54 @@ onMounted(async () => {
value: 'china',
label: '中国',
children: [
{
value: 'north',
label: '华北',
children: [
{ value: 'beijing', label: '北京' },
{ value: 'tianjin', label: '天津' },
{ value: 'hebei', label: '河北' }
]
},
{
value: 'east',
label: '华东',
children: [
{ value: 'shanghai', label: '上海' },
{ value: 'jiangsu', label: '江苏' },
{ value: 'zhejiang', label: '浙江' }
]
},
{
value: 'south',
label: '华南',
children: [
{ value: 'guangdong', label: '广东' },
{ value: 'guangxi', label: '广西' },
{ value: 'hainan', label: '海南' }
]
},
{
value: 'southwest',
label: '西南',
children: [
{ value: 'sichuan', label: '四川' },
{ value: 'chongqing', label: '重庆' },
{ value: 'yunnan', label: '云南' }
{ value: 'yunnan', label: '云南' },
{ value: 'guizhou', label: '贵州' },
{ value: 'xizang', label: '西藏' }
]
},
{
value: 'northeast',
label: '东北',
children: [
{ value: 'liaoning', label: '辽宁' },
{ value: 'jilin', label: '吉林' },
{ value: 'heilongjiang', label: '黑龙江' }
]
},
{
value: 'northwest',
label: '西北',
children: [
{ value: 'shaanxi', label: '陕西' },
{ value: 'gansu', label: '甘肃' },
{ value: 'qinghai', label: '青海' },
{ value: 'ningxia', label: '宁夏' },
{ value: 'xinjiang', label: '新疆' }
]
},
{
value: 'central',
label: '华中',
children: [
{ value: 'henan', label: '河南' },
{ value: 'hubei', label: '湖北' },
{ value: 'hunan', label: '湖南' }
]
},
{
value: 'northchina',
label: '华北',
children: [
{ value: 'tianjin', label: '天津' },
{ value: 'hebei', label: '河北' },
{ value: 'shanxi', label: '山西' },
{ value: 'neimenggu', label: '内蒙古' }
]
}
]
@@ -756,10 +770,12 @@ onMounted(async () => {
children: [
{ value: 'asia', label: '亚洲' },
{ value: 'europe', label: '欧洲' },
{ value: 'america', label: '美洲' }
{ value: 'america', label: '美洲' },
{ value: 'africa', label: '非洲' },
{ value: 'oceania', label: '大洋洲' }
]
}
]
]
// 设置对话框宽度响应式
updateDialogWidth()
File diff suppressed because it is too large Load Diff
+200 -32
View File
@@ -22,8 +22,7 @@
style="width: 100%"
border
>
<el-table-column prop="instance_id" label="ID" width="100" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column label="创建时间" width="180">
<template #default="scope">
@@ -37,12 +36,33 @@
</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>
<el-tag :type="scope.row.state == 0 || scope.row.state == 1
? 'info'
: scope.row.state == 2
? 'success'
: scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6
? 'warning'
: 'danger'
">{{
scope.row.state == 0
? "未支付"
: scope.row.state == 1
? "未创建"
: scope.row.state == 2
? "已启动"
: scope.row.state == 3
? "已关机"
: scope.row.state == 4
? "重装中"
: scope.row.state == 5
? "创建快照中"
: scope.row.state == 6
? "恢复快照中"
: "未知状态"
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<el-table-column label="操作" width="300" fixed="right">
<template #default="scope">
<el-button
type="primary"
@@ -52,33 +72,43 @@
>
<el-icon><menu /></el-icon>管理
</el-button>
<el-button
type="success"
<!-- <el-button
type="primary"
link
size="small"
@click="handleStart(scope.row)"
:disabled="scope.row.state === 'running'"
:disabled="scope.row.state == 2 || scope.row.state == 0 || scope.row.state == 1 || scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6"
>
<el-icon><video-play /></el-icon>启动
</el-button>
<el-button
type="warning"
开机
</el-button> -->
<!-- <el-button
type="primary"
link
size="small"
@click="handleStop(scope.row)"
:disabled="scope.row.state === 'stopped'"
:disabled="scope.row.state != 2"
>
<el-icon><video-pause /></el-icon>停止
</el-button>
关机
</el-button> -->
<!-- <el-dropdown trigger="click" @command="(command) => handleCommand(command, scope.row)" style="transform: translateY(3px) translateX(10px);">
<el-button
type="info"
type="primary"
link
size="small"
@click="handleRestart(scope.row)"
:disabled="scope.row.state !== 'running'"
>
<el-icon><refresh-right /></el-icon>重启
更多
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="scope.row.state != 2" command="restart">重启</el-dropdown-item>
<el-dropdown-item divided :disabled="scope.row.state == 0 || scope.row.state == 1 || scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6" command="reinstall">重装系统</el-dropdown-item>
<el-dropdown-item :disabled="scope.row.state == 0 || scope.row.state == 1 || scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6" command="snapshot">快照管理</el-dropdown-item>
<el-dropdown-item :disabled="scope.row.state == 0 || scope.row.state == 1 || scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6" command="enterRescue">进入救援系统</el-dropdown-item>
<el-dropdown-item :disabled="scope.row.state == 0 || scope.row.state == 1 || scope.row.state == 4 || scope.row.state == 5 || scope.row.state == 6" command="exitRescue">退出救援系统</el-dropdown-item>
<el-dropdown-item divided :disabled="scope.row.state == 0 || scope.row.state == 1" command="console">虚拟机控制台</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
</template>
</el-table-column>
</el-table>
@@ -102,13 +132,17 @@ 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
Search, Refresh, Menu, VideoPlay, VideoPause, RefreshRight, ArrowDown
} from '@element-plus/icons-vue';
import {
getContainer,
startInstance,
stopInstance,
restartInstance
restartInstance,
getInstanceList,
reinstallI,
rescueInstance,
exitRescueInstance
} from '@/utils/acs/server';
const props = defineProps({
@@ -130,13 +164,14 @@ const searchKey = ref('');
const fetchVmList = async () => {
loading.value = true;
try {
const response = await getContainer({
const response = await getInstanceList({
server_id: props.ID,
page: currentPage.value,
count: pageSize.value,
key: searchKey.value,
user_id: ''
});
console.log("获取虚拟机列表:",response);
if (response && response.data && response.data.code === 200) {
vmList.value = response.data.data || [];
@@ -152,14 +187,19 @@ const fetchVmList = async () => {
}
};
function Formater(row){
return row.hostname || '未设置'
}
// 获取状态类型
const getStatusType = (state) => {
console.log("获取状态类型:",state)
switch (state) {
case 'running':
case '0':
return 'success';
case 'stopped':
case '1':
return 'danger';
case 'paused':
case '2':
return 'warning';
default:
return 'info';
@@ -169,15 +209,15 @@ const getStatusType = (state) => {
// 获取状态文本
const getStatusText = (state) => {
switch (state) {
case 'running':
case '0':
return '运行中';
case 'stopped':
case '1':
return '已停止';
case 'paused':
case '2':
return '已暂停';
case 'creating':
case '3':
return '创建中';
case 'error':
case '4':
return '错误';
default:
return '未知';
@@ -212,7 +252,7 @@ const handleSizeChange = (val) => {
// 管理虚拟机
const handleManage = (row) => {
router.push(`/servers/vm?instance_id=${row.instance_id}`);
router.push(`/servers/vm?instance_id=${row.id}`);
};
// 启动虚拟机
@@ -281,6 +321,112 @@ const handleRestart = async (row) => {
}
};
// 重装系统
const handleReinstall = async (row) => {
try {
ElMessageBox.confirm('重装系统将清除所有数据,确定要继续吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
router.push(`/servers/reinstall?instance_id=${row.instance_id}`);
}).catch(() => {});
} catch (error) {
console.error('重装系统出错:', error);
ElMessage.error('重装系统出错');
}
};
// 快照管理
const handleSnapshot = (row) => {
router.push(`/servers/snapshots?instance_id=${row.instance_id}`);
};
// 进入救援系统
const handleEnterRescue = async (row) => {
try {
ElMessageBox.confirm('确定要进入救援系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await rescueInstance(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 handleExitRescue = async (row) => {
try {
ElMessageBox.confirm('确定要退出救援系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await exitRescueInstance(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 handleConsole = (row) => {
window.open(`/console?instance_id=${row.instance_id}`, '_blank');
};
// 处理下拉菜单命令
const handleCommand = (command, row) => {
switch (command) {
case 'start':
handleStart(row);
break;
case 'stop':
handleStop(row);
break;
case 'restart':
handleRestart(row);
break;
case 'reinstall':
handleReinstall(row);
break;
case 'snapshot':
handleSnapshot(row);
break;
case 'enterRescue':
handleEnterRescue(row);
break;
case 'exitRescue':
handleExitRescue(row);
break;
case 'console':
handleConsole(row);
break;
default:
break;
}
};
// 监听ID变化
watch(() => props.ID, (newVal) => {
if (newVal) {
@@ -311,4 +457,26 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
}
/* 添加下拉菜单样式 */
:deep(.el-dropdown-menu) {
min-width: 120px;
}
:deep(.el-dropdown-menu__item) {
padding: 8px 16px;
font-size: 14px;
line-height: 1.5;
}
:deep(.el-dropdown-menu__item--divided) {
margin-top: 6px;
border-top: 1px solid #ebeef5;
margin-bottom: 6px;
}
:deep(.el-dropdown-menu__item--divided:before) {
height: 6px;
margin-top: -6px;
}
</style>
+82 -19
View File
@@ -225,18 +225,22 @@
:type="scope.row.container_state == 0 || scope.row.container_state == 1
? 'info'
: scope.row.container_state == 2
? 'success'
: 'danger'"
? 'danger'
: scope.row.container_state == 3
? 'danger'
: 'success'"
effect="light"
>
{{
scope.row.container_state == 0
? "未支付"
? "未构建"
: scope.row.container_state == 1
? "未开通"
? "已构建"
: scope.row.container_state == 2
? "开机"
: "关机"
? "构建失败"
: scope.row.container_state == 3
? "已删除"
: "未知状态"
}}
</el-tag>
</template>
@@ -378,6 +382,7 @@
v-model="spec_form.cpu"
:placeholder="TypeData == 'dockerContainer' ? 'CPU 限制 (1核=1000毫核)' : 'CPU 限制'"
type="number"
min="1"
>
<template #append>{{ TypeData == 'dockerContainer' ? '毫核' : '核' }}</template>
</el-input>
@@ -432,28 +437,28 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="允许开放端口数">
<el-input v-model="spec_form.port_num" placeholder="默认允许开放端口数" type="number">
<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">
<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">
<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">
<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>
@@ -609,6 +614,8 @@ import {
} from "@/utils/acs/server";
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,
@@ -660,11 +667,30 @@ const initData = async () => {
// 获取实例规格
await GetSpecs();
// 获取用户信息并设置用户类型
try {
const userInfoRes = await getUserInfo();
console.log("获取用户信息", userInfoRes);
if (userInfoRes && userInfoRes.data && userInfoRes.data.data) {
// 根据API返回的用户信息设置用户类型
const userType = userInfoRes.data.is_admin == true ? "1" : "0";
localStorage.setItem("user_id", userInfoRes.real_name.UserId);
localStorage.setItem("user_type", userType);
console.log("获取到用户类型", userType);
}
} catch (error) {
console.error("获取用户信息失败:", error);
// 如果获取失败,默认设置为管理员权限确保功能可用
localStorage.setItem("user_type", "1");
}
// 获取所有容器
let usertype = localStorage.getItem("user_type");
console.log("用户类型", usertype);
if (usertype == "1") {
containerBox.server_id = route.query.server_id;
let cons = await getContainer(containerBox);
console.log("获取容器列表", cons);
if (cons && cons.data) {
user_servers.value = cons.data.data || [];
total.value = cons.data.count || 0;
@@ -900,10 +926,11 @@ const editSpec = async () => {
if (addOrChange.value) {
const res = await addServerPlan({
...spec_form,
server_id: route.query.server_id
server_id: route.query.server_id,
server_type:TypeData.value
});
if (res.data.code == 200) {
if (res.code == 200) {
ElNotification({
title: '添加成功',
message: `已成功添加规格 "${spec_form.name}"`,
@@ -916,7 +943,32 @@ const editSpec = async () => {
return;
}
} else {
const res = await editServerPlan(spec_form);
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({
@@ -1018,7 +1070,7 @@ const spec_form = reactive({
bandwidth_tx: "",
threshold_rx: "",
threshold_tx: "",
port_num: "",
port_num: 0,
snapshot_num: "",
number: "",
volume_price: "",
@@ -1030,22 +1082,33 @@ const spec_form = reactive({
function show_spec(data = null) {
if (!data) {
// 当data未传值,视为新增实例规格
// 遍历form对象的每个键值对
// 重置表单时根据服务器类型设置不同默认值
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 {
// data传值,视为修改实例规格
// 遍历data对象的每个键值对
// 虚拟机默认值
spec_form.port_num = '5';
spec_form.snapshot_num = '3';
spec_form.min_iops = '1000';
spec_form.max_iops = '5000';
}
} else {
// 复制传入的数据
Object.keys(data).forEach(key => {
if (key in spec_form) {
spec_form[key] = data[key];
}
});
// 确保服务器类型正确
spec_form.server_type = TypeData.value;
}
}