jcloud/dashboard/src2/pages/NewJsiteServer.vue

778 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 页面头部 -->
<div class="sticky top-0 z-10 shrink-0">
<Header>
<Breadcrumbs
:items="[
{ label: '服务器', route: '/jsite-servers' },
{ label: '新建服务器', route: '/jsite-servers/new' }
]"
/>
</Header>
</div>
<!-- 主要内容区域 -->
<div class="mx-auto max-w-2xl px-5">
<!-- 第一步选择服务器配置和支付方式 -->
<div v-if="!showPaymentProcessing" class="space-y-12 pb-[50vh] pt-12">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 mb-2">新建服务器</h1>
<p class="text-sm text-gray-600">
请选择服务器配置和支付方式点击"创建"后将自动为您开通
</p>
</div>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">区域选择</label>
<select v-model="selectedRegionId" class="w-full border rounded px-3 py-2" required @change="onRegionChange">
<option value="">请选择区域</option>
<option v-for="region in regions" :key="getRegionId(region)" :value="getRegionId(region)">
{{ getRegionName(region) }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">镜像选择Jsite服务器推荐选择Ubuntu-22.04</label>
<select v-model="selectedImageId" class="w-full border rounded px-3 py-2" required @change="onImageChange">
<option value="">请选择镜像</option>
<option v-for="image in images" :key="getImageId(image)" :value="getImageId(image)">
{{ getImageDisplayName(image) }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">套餐选择</label>
<select v-model="selectedPlanId" class="w-full border rounded px-3 py-2" required :disabled="!selectedImageId">
<option value="">请选择套餐</option>
<option v-for="plan in filteredPlans" :key="plan.plan_id" :value="plan.plan_id">
{{ getPlanDisplayName(plan) }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">购买时长</label>
<select v-model="period" class="w-full border rounded px-3 py-2">
<option v-for="p in periods" :key="p.value" :value="p.value">{{ p.label }}</option>
</select>
</div>
<!-- 价格信息显示 -->
<div v-if="selectedPlanId && selectedImageId" class="border-t border-gray-200 pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-600">月度费用</div>
<div class="font-medium">
¥ {{ getSelectedPlanPrice() }}
<span class="text-gray-500 text-sm">(月付)</span>
</div>
</div>
<div class="flex justify-between items-center mt-2">
<div class="text-sm text-gray-600">购买时长</div>
<div class="font-medium">{{ period }} 个月</div>
</div>
<div class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div>¥ {{ getTotalAmount() }}</div>
</div>
</div>
<!-- 支付方式选择 -->
<div class="border-t border-gray-200 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-3">
选择支付方式
</label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
<button
class="p-4 border rounded-lg flex flex-col items-center justify-center hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'balance', 'border-gray-200': selectedPaymentMethod !== 'balance'}"
@click="selectedPaymentMethod = 'balance'"
>
<span class="text-gray-800 font-medium">余额支付</span>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'alipay', 'border-gray-200': selectedPaymentMethod !== 'alipay'}"
@click="selectedPaymentMethod = 'alipay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<AlipayLogo class="h-8" />
</div>
</div>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'wechatpay', 'border-gray-200': selectedPaymentMethod !== 'wechatpay'}"
@click="selectedPaymentMethod = 'wechatpay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<WeChatPayLogo class="h-8" />
</div>
</div>
</button>
</div>
</div>
<div v-if="error" class="p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
<!-- 创建按钮 -->
<div class="pt-4">
<button
type="button"
class="w-full px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none"
@click="createInstance"
:disabled="isLoading"
>
{{ isLoading ? '处理中...' : '创建' }}
</button>
</div>
</div>
</div>
<!-- 第二步处理支付 -->
<div v-else class="space-y-12 pb-[50vh] pt-12">
<!-- 加载中状态 -->
<div v-if="isProcessingPayment" class="text-center py-8">
<div class="h-12 w-12 mx-auto animate-spin text-blue-600 mb-4 flex items-center justify-center">
<i class="fe fe-loader text-3xl"></i>
</div>
<p class="text-gray-700 text-lg">正在处理支付请稍候...</p>
</div>
<!-- 支付成功状态 -->
<div v-else-if="paymentSuccess" class="text-center py-8">
<div class="flex justify-center mb-6">
<div class="rounded-full bg-green-100 p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<h3 class="text-xl font-medium text-gray-900 mb-2">支付成功</h3>
<p class="text-gray-600 mb-4">
您的订单已支付成功服务器记录已创建服务器正在后台创建中创建并启动需要 3-5 分钟请耐心等待
</p>
<p class="text-gray-500 text-sm">
您可以在服务器列表中查看服务器状态创建完成后状态将更新为"Running"
</p>
<div class="mt-6">
<button
type="button"
class="px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none"
@click="goToServerList"
>
返回服务器列表
</button>
</div>
</div>
<!-- 微信支付状态 -->
<div v-else-if="selectedPaymentMethod === 'wechatpay' && paymentQrCode" class="text-center">
<div class="relative mb-6">
<div class="flex items-center justify-center">
<WeChatPayLogo class="h-10" />
</div>
</div>
<div class="text-center mb-6">
<div class="text-sm text-gray-600 mb-2">扫一扫付款</div>
<div class="text-3xl font-bold text-[#FF0036]">{{ order?.total_amount }} </div>
</div>
<div class="flex justify-center">
<div class="qrcode-container bg-white p-4 border border-gray-100 shadow-sm rounded-lg">
<img
:src="paymentQrCodeImage"
alt="微信支付二维码"
class="qrcode-image"
/>
</div>
</div>
<div class="mt-6 text-center">
<p class="text-gray-600">
请使用微信扫描二维码完成支付
</p>
<p class="text-gray-500 text-sm mt-2">二维码有效期 15 分钟</p>
</div>
</div>
<!-- 支付宝支付状态 -->
<div v-else-if="selectedPaymentMethod === 'alipay' && paymentUrl" class="text-center py-6">
<div class="mb-6">
<AlipayLogo class="h-10 mx-auto" />
</div>
<h3 class="text-lg font-medium text-gray-900 mb-4">请在新页面完成支付宝支付</h3>
<p class="text-gray-600 mb-6">
如果没有自动跳转请点击下方按钮打开支付页面
</p>
<div class="space-y-4">
<button
class="w-full px-6 py-3 bg-[#1677FF] text-white rounded-lg hover:bg-[#0E5FD8] transition-colors shadow-sm"
@click="window.open(paymentUrl, '_blank')"
>
打开支付页面
</button>
<p class="text-gray-600 text-sm mt-4">
支付完成后请稍等片刻系统会自动刷新页面
</p>
</div>
</div>
<div v-if="error" class="p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
</div>
</div>
</template>
<script>
import { toast } from 'vue-sonner';
import { DashboardError } from '../utils/error';
import AlipayLogo from '../logo/AlipayLogo.vue';
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
import router from '../router';
export default {
name: 'NewJsiteServer',
components: {
AlipayLogo,
WeChatPayLogo
},
data() {
return {
selectedRegionId: '',
selectedPlanId: '',
selectedImageId: '',
period: 1,
selectedPaymentMethod: null, // 支付方式
regions: [],
plans: [],
images: [],
periods: [
{ value: 1, label: '1个月' },
{ value: 3, label: '3个月' },
{ value: 6, label: '6个月' },
{ value: 12, label: '1年' },
{ value: 24, label: '2年' },
{ value: 36, label: '3年' }
],
isLoading: false,
error: null,
success: false,
// 支付相关状态
order: null,
server: null, // 服务器记录信息
showPaymentProcessing: false,
paymentSuccess: false,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null
};
},
resources: {
aliyunRegions() {
return {
url: 'jcloud.api.aliyun_server_light.get_aliyun_regions',
onSuccess(response) {
// 后端返回格式: {success: true, data: {...}, message: '...'}
const data = response.data || response;
// 处理不同的数据结构可能性
let regionsData = [];
if (data.regions) {
// 直接有regions字段
regionsData = data.regions;
} else if (data.Regions) {
// 阿里云API可能返回Regions字段
regionsData = data.Regions;
} else if (Array.isArray(data)) {
// 直接是数组
regionsData = data;
} else if (data.body && data.body.regions) {
// 嵌套在body中
regionsData = data.body.regions;
} else if (data.body && data.body.Regions) {
// 嵌套在body中字段名大写
regionsData = data.body.Regions;
}
if (Array.isArray(regionsData) && regionsData.length > 0) {
this.regions = regionsData;
} else {
this.error = '区域数据格式不正确或为空';
}
},
onError(error) {
this.error = error.message || '获取区域失败';
}
};
},
aliyunPlans() {
return {
url: 'jcloud.api.aliyun_server_light.get_aliyun_plans',
onSuccess(response) {
const data = response.data || response;
this.plans = data.plans || [];
},
onError(error) {
this.error = error.message || '获取套餐失败';
}
};
},
aliyunImages() {
return {
url: 'jcloud.api.aliyun_server_light.get_aliyun_images',
onSuccess(response) {
const data = response.data || response;
// 处理不同的数据结构可能性
let imagesData = [];
if (data.images) {
// 直接有images字段
imagesData = data.images;
} else if (data.Images) {
// 阿里云API可能返回Images字段
imagesData = data.Images;
} else if (Array.isArray(data)) {
// 直接是数组
imagesData = data;
} else if (data.body && data.body.images) {
// 嵌套在body中
imagesData = data.body.images;
} else if (data.body && data.body.Images) {
// 嵌套在body中字段名大写
imagesData = data.body.Images;
}
if (Array.isArray(imagesData) && imagesData.length > 0) {
this.images = imagesData;
} else {
this.error = '镜像数据格式不正确或为空';
}
},
onError(error) {
this.error = error.message || '获取镜像失败';
}
};
},
// 创建服务器订单
createServerOrder() {
return {
url: 'jcloud.api.aliyun_server_light.create_server_order',
validate() {
if (!this.selectedRegionId || !this.selectedPlanId || !this.selectedImageId) {
throw new DashboardError('请选择完整配置');
}
if (!this.selectedPaymentMethod) {
throw new DashboardError('请选择支付方式');
}
},
onSuccess(data) {
if (!data.success) {
this.error = data.message || '创建服务器订单失败';
return;
}
// 显示订单支付界面
this.order = data.order;
this.server = data.server; // 保存服务器记录信息
this.showPaymentProcessing = true;
// 立即处理支付
this.processPayment();
},
onError(error) {
this.error = error.message || '创建服务器订单失败';
}
};
},
// 处理余额支付
processBalancePayment() {
return {
url: 'jcloud.api.billing.process_balance_payment_for_server_order',
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
if (response.status === "Error" || response.success === false) {
toast.error(response.message || '支付失败,请确保余额充足');
this.error = response.message || '余额不足';
this.isProcessingPayment = false;
return;
}
const orderData = {
...this.order,
...(response.order || {}),
status: '已支付'
};
// 同时发送服务器记录信息
const serverData = {
...this.server
};
this.isProcessingPayment = false;
this.paymentSuccess = true;
// 支付成功后跳转到服务器列表
setTimeout(() => {
this.$router.push('/jsite-servers');
}, 2000);
},
onError(error) {
this.error = error.message || '余额支付处理失败';
this.isProcessingPayment = false;
}
};
},
// 处理支付宝支付
processAlipayPayment() {
return {
url: 'jcloud.api.billing.process_alipay_order',
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
this.paymentUrl = response.payment_url;
window.open(response.payment_url, '_blank');
toast.success('支付页面已在新窗口打开');
// 开始轮询支付状态
this.startPaymentCheck();
},
onError(error) {
this.error = error.message || '支付宝支付处理失败';
this.isProcessingPayment = false;
}
};
},
// 处理微信支付
processWechatPayment() {
return {
url: 'jcloud.api.billing.process_wechatpay_order',
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
this.paymentQrCode = response.payment_url;
this.paymentQrCodeImage = response.qr_code_image || null;
// 开始轮询支付状态
this.startPaymentCheck();
},
onError(error) {
this.error = error.message || '微信支付处理失败';
this.isProcessingPayment = false;
}
};
},
// 检查支付状态
checkPaymentStatus() {
return {
url: 'jcloud.api.billing.check_order_payment_status',
params: {
order_id: this.order?.order_id
},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(data) {
if (data && data.status === '已支付') {
// 支付成功,停止轮询
this.stopPaymentCheck();
const orderData = { ...this.order, ...(data.order || {}) };
// 同时发送服务器记录信息
const serverData = {
...this.server
};
this.paymentSuccess = true;
// 支付成功后跳转到服务器列表
setTimeout(() => {
this.$router.push('/jsite-servers');
}, 2000);
}
}
};
},
},
computed: {
filteredPlans() {
// 如果没有选择镜像,返回空数组
if (!this.selectedImageId) {
return [];
}
// 获取选择的镜像信息
const selectedImage = this.images.find(img => this.getImageId(img) === this.selectedImageId);
if (!selectedImage) {
return [];
}
// 获取镜像平台信息
const imagePlatform = selectedImage.platform || '';
// 根据镜像平台过滤套餐
return this.plans.filter(plan => {
const supportPlatform = plan.support_platform;
if (!supportPlatform) {
return true; // 如果没有support_platform信息默认显示
}
try {
// 解析support_platform字段JSON字符串格式
const platforms = JSON.parse(supportPlatform);
return platforms.includes(imagePlatform);
} catch (e) {
// 如果解析失败,尝试字符串匹配
return supportPlatform.includes(imagePlatform);
}
});
}
},
beforeUnmount() {
this.stopPaymentCheck();
},
mounted() {
this.$resources.aliyunRegions.submit();
},
methods: {
async onRegionChange() {
this.selectedPlanId = '';
this.selectedImageId = '';
this.plans = [];
this.images = [];
if (this.selectedRegionId) {
// 只加载镜像,套餐将在选择镜像后加载
await this.$resources.aliyunImages.submit({
region_id: this.selectedRegionId,
image_type: 'system' // 明确指定镜像类型
});
}
},
async onImageChange() {
this.selectedPlanId = '';
this.plans = [];
if (this.selectedImageId) {
await this.$resources.aliyunPlans.submit({
region_id: this.selectedRegionId
});
}
},
async createInstance() {
if (!this.selectedRegionId || !this.selectedPlanId || !this.selectedImageId) {
this.error = '请选择完整配置';
return;
}
if (!this.selectedPaymentMethod) {
this.error = '请选择支付方式';
return;
}
this.error = null;
this.isLoading = true;
await this.$resources.createServerOrder.submit({
plan_id: this.selectedPlanId,
image_id: this.selectedImageId,
period: this.period,
region_id: this.selectedRegionId,
payment_method: this.selectedPaymentMethod
});
this.isLoading = false;
},
startPaymentCheck() {
this.isProcessingPayment = false;
this.checkInterval = setInterval(() => {
this.$resources.checkPaymentStatus.submit();
}, 3000);
// 15分钟后停止检查
setTimeout(() => {
this.stopPaymentCheck();
}, 900000);
},
stopPaymentCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
},
processPayment() {
this.isProcessingPayment = true;
this.error = null;
if (!this.order || !this.order.order_id) {
this.error = '订单信息不完整,请重试';
this.isProcessingPayment = false;
return;
}
if (this.selectedPaymentMethod === 'balance') {
this.$resources.processBalancePayment.submit({
order_id: this.order.order_id
});
} else if (this.selectedPaymentMethod === 'alipay') {
this.$resources.processAlipayPayment.submit({
order_id: this.order.order_id
});
} else if (this.selectedPaymentMethod === 'wechatpay') {
this.$resources.processWechatPayment.submit({
order_id: this.order.order_id
});
}
},
goToServerList() {
this.$router.push('/jsite-servers');
},
getRegionId(region) {
return region.region_id || region.RegionId || region.id;
},
getRegionName(region) {
return region.local_name || region.region_name || region.RegionName || region.name;
},
getPlanDisplayName(plan) {
// 根据阿里云API返回的数据结构显示套餐信息
const cpu = plan.core || plan.cpu || '未知';
const memory = plan.memory || '未知';
const disk = plan.disk_size || plan.system_disk_size || '未知';
const bandwidth = plan.bandwidth || '未知';
const price = plan.origin_price || '未知';
const planType = plan.plan_type || '';
const publicIpNum = plan.public_ip_num || 0;
// 处理内存显示如果是小数转换为MB
let memoryDisplay = memory;
if (typeof memory === 'number' && memory < 1) {
memoryDisplay = Math.round(memory * 1024) + 'MB';
} else if (typeof memory === 'number') {
memoryDisplay = memory + 'GB';
}
// 处理带宽显示
let bandwidthDisplay = bandwidth;
if (typeof bandwidth === 'number') {
bandwidthDisplay = bandwidth + 'Mbps';
}
// 根据plan_type添加类型前缀
let typePrefix = '';
switch (planType) {
case 'NORMAL':
typePrefix = '通用型 - ';
break;
case 'MULTI_IP':
typePrefix = '多公网IP型 - ';
break;
case 'INTERNATIONAL':
typePrefix = '国际型 - ';
break;
case 'CAPACITY':
typePrefix = '容量型 - ';
break;
default:
typePrefix = '';
}
return `${typePrefix}${cpu}核/${memoryDisplay}/${disk}GB/${bandwidthDisplay}/${publicIpNum}个公网IP - ¥${price}/月`;
},
getSelectedPlanPrice() {
const selectedPlan = this.filteredPlans.find(plan => plan.plan_id === this.selectedPlanId);
return selectedPlan ? (selectedPlan.origin_price || 0) : 0;
},
getTotalAmount() {
const monthlyPrice = this.getSelectedPlanPrice();
return (monthlyPrice * this.period).toFixed(2);
},
getImageId(image) {
return image.image_id || image.ImageId || image.id;
},
getImageDisplayName(image) {
// 根据阿里云API返回的数据结构显示镜像信息
const name = image.image_name || image.name || image.ImageName || image.Name || '未知镜像';
const platform = image.platform || '';
const description = image.description || '';
// 显示格式:镜像名称 (平台)
if (platform) {
return `${name} (${platform})`;
}
return name;
},
getPaymentMethodName(method) {
const methodNames = {
'balance': '余额支付',
'alipay': '支付宝',
'wechatpay': '微信支付'
};
return methodNames[method] || '未知方式';
}
},
};
</script>
<style scoped>
.qrcode-container {
height: 250px;
width: 250px;
margin: 0 auto;
padding: 5px;
border: 1px solid #e5e7eb;
border-radius: 12px;
transition: all 0.3s ease;
}
.qrcode-image {
height: 100%;
width: 100%;
object-fit: contain;
}
/* 添加渐入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>