Merge pull request 'feat:添加服务器新建容器' (#4) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m19s
Build and Deploy Vue3 / deploy (push) Successful in 20m53s

Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#4
This commit was merged in pull request #4.
This commit is contained in:
2025-10-04 23:25:58 +08:00
3 changed files with 618 additions and 14 deletions
+8 -1
View File
@@ -53,7 +53,14 @@ export const selectServerPlan = data => {
}
});
};
/**新增容器 */
export const addContainer = data => {
return http2.post("/v1/admin/container/add_container", data, {
headers: {
"Content-Type": "multipart/form-data"
}
});
};
/**删除容器网络 */
export const deleteContainerNetwork = data => {
return http2.post("/v1/user/container/delete_connect", data, {
+4 -4
View File
@@ -93,10 +93,10 @@ const loginRules = {
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
]
// password: [
// { required: true, message: '请输入密码', trigger: 'blur' },
// { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
// ]
}
const forgetPassword = () => {
+606 -9
View File
@@ -541,13 +541,22 @@
<el-tab-pane v-if="TypeData == 'dockerContainer'" label="容器列表">
<div class="tab-header">
<h3 class="tab-title">容器管理</h3>
<el-input
v-model="containerBox.key"
placeholder="搜索容器..."
class="search-input"
:prefix-icon="Search"
clearable
/>
<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
@@ -956,11 +965,261 @@
</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="请选择镜像"
style="width: 100%"
clearable
:loading="containerMirrorLoading"
@focus="fetchContainerMirrorList"
>
<el-option
v-for="mirror in containerMirrorList"
:key="mirror.id"
:label="mirror.name"
:value="mirror.image_id"
>
<span style="float: left">{{ mirror.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 } from "vue";
import { reactive, ref, onMounted, watch, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import {
selectServer,
@@ -977,8 +1236,10 @@ import {
getDiskInfo,
getRealDisk,
getTraffic,
getTotalTraffic
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";
@@ -1204,6 +1465,7 @@ const initData = async () => {
// 并行获取服务器硬件和流量信息(不阻塞主要功能)
Promise.allSettled([
getDiskInfoData(),
getRealDiskData(),
getTrafficData(),
@@ -2015,6 +2277,287 @@ const getit = async () => {
}
};
// 添加容器相关状态
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' },
{ type: 'number', min: 1, max: 65535, message: '请输入有效的端口(1-65535)', 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(route.query.server_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>
@@ -2362,6 +2905,12 @@ import { ElMessageBox } from 'element-plus';
width: 240px;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.action-btns {
display: flex;
gap: 8px;
@@ -2451,6 +3000,54 @@ import { ElMessageBox } from 'element-plus';
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 {