jcloud/dashboard/src2/pages/NewJsiteServer.vue

726 lines
25 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>
<Dialog :options="{ title: '新建 Jsite 服务器', size: 'lg' }" v-model="show">
<!-- 第一步选择服务器配置和支付方式 -->
<template v-if="!showPaymentProcessing" #body-content>
<div class="p-4 sm:p-6">
<div class="mb-6">
<p class="mt-1 text-sm text-gray-600">
请选择服务器配置并填写相关信息点击"创建"后将自动为您开通
</p>
</div>
<div class="mb-4">
<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 v-if="regions.length === 0" class="mt-2 text-sm text-gray-500">
正在加载地域列表...
</div>
<div v-if="regions.length > 0" class="mt-2 text-sm text-gray-500">
已加载 {{ regions.length }} 个地域
</div>
</div>
<div class="mb-4">
<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>
<option value="">请选择套餐</option>
<option v-for="plan in plans" :key="plan.plan_id" :value="plan.plan_id">
{{ getPlanDisplayName(plan) }}
</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">镜像选择</label>
<select v-model="selectedImageId" class="w-full border rounded px-3 py-2" required>
<option value="">请选择镜像</option>
<option v-for="image in images" :key="getImageId(image)" :value="getImageId(image)">
{{ getImageDisplayName(image) }}
</option>
</select>
</div>
<div class="mb-4">
<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="mb-6 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="mb-6 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="mt-4 p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
</template>
<!-- 第二步处理支付 -->
<template v-else #body-content>
<div class="p-4 sm:p-6">
<!-- 加载中状态 -->
<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>
<!-- 微信支付状态 -->
<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="mt-4 p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
</template>
<template #actions>
<div class="w-full">
<button v-if="!showPaymentProcessing && !paymentSuccess"
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 class="flex justify-between w-full" v-else>
<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 ml-auto"
@click="close"
v-if="paymentSuccess"
>
关闭
</button>
</div>
</div>
</template>
</Dialog>
</template>
<script>
import { toast } from 'vue-sonner';
import { Dialog } from 'jingrow-ui';
import { DashboardError } from '../utils/error';
import AlipayLogo from '../logo/AlipayLogo.vue';
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
export default {
name: 'NewJsiteServer',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
data() {
return {
show: true,
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) {
console.log('地域API完整响应:', response);
// 后端返回格式: {success: true, data: {...}, message: '...'}
const data = response.data || response;
console.log('提取的data部分:', data);
// 处理不同的数据结构可能性
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;
}
console.log('解析后的地域数据:', regionsData);
if (Array.isArray(regionsData) && regionsData.length > 0) {
this.regions = regionsData;
console.log('成功设置地域数据:', this.regions);
} else {
this.error = '地域数据格式不正确或为空';
console.error('地域数据格式错误:', data);
}
},
onError(error) {
console.error('获取地域失败:', 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) {
console.log('镜像API完整响应:', response);
const data = response.data || response;
console.log('镜像API data部分:', data);
// 处理不同的数据结构可能性
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;
}
console.log('解析后的镜像数据:', imagesData);
if (Array.isArray(imagesData) && imagesData.length > 0) {
this.images = imagesData;
console.log('成功设置镜像数据:', this.images);
} else {
this.error = '镜像数据格式不正确或为空';
console.error('镜像数据格式错误:', data);
}
},
onError(error) {
console.error('获取镜像失败:', 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;
}
console.log('订单创建成功,完整响应:', data);
console.log('订单ID:', data.order?.order_id);
console.log('服务器记录:', data.server);
// 显示订单支付界面
this.order = data.order;
this.server = data.server; // 保存服务器记录信息
this.showPaymentProcessing = true;
// 立即处理支付
this.processPayment();
},
onError(error) {
console.error('创建服务器订单失败:', 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.$emit('success', { order: orderData, server: serverData });
this.isProcessingPayment = false;
this.paymentSuccess = true;
},
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.$emit('success', { order: orderData, server: serverData });
this.paymentSuccess = true;
}
}
};
},
},
beforeUnmount() {
this.stopPaymentCheck();
},
mounted() {
console.log('组件mounted开始获取地域列表');
this.$resources.aliyunRegions.submit();
},
methods: {
async onRegionChange() {
console.log('地域变更选择的地域ID:', this.selectedRegionId);
this.selectedPlanId = '';
this.selectedImageId = '';
this.plans = [];
this.images = [];
if (this.selectedRegionId) {
console.log('开始获取套餐和镜像数据...');
await this.$resources.aliyunPlans.submit({ region_id: this.selectedRegionId });
await this.$resources.aliyunImages.submit({
region_id: this.selectedRegionId,
image_type: 'system' // 明确指定镜像类型
});
}
},
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) {
console.error('缺少订单ID无法继续支付');
this.error = '订单信息不完整,请重试';
this.isProcessingPayment = false;
return;
}
console.log('准备处理支付,订单信息:', {
id: this.order.order_id,
method: this.selectedPaymentMethod
});
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
});
}
},
close() {
this.show = false;
this.$emit('success');
},
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 price = plan.origin_price || '未知';
// 处理内存显示如果是小数转换为MB
let memoryDisplay = memory;
if (typeof memory === 'number' && memory < 1) {
memoryDisplay = Math.round(memory * 1024) + 'MB';
} else if (typeof memory === 'number') {
memoryDisplay = memory + 'GB';
}
return `${cpu}核/${memoryDisplay}/${disk}GB - ¥${price}/月`;
},
getSelectedPlanPrice() {
const selectedPlan = this.plans.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>