jcloud/dashboard/src2/components/JsiteServerUpgradeDialog.vue

644 lines
22 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: '服务器升级', 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-6 border-t border-gray-200 pt-4">
<h3 class="text-lg font-medium text-gray-900 mb-4">当前配置</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-600">CPU</div>
<div class="font-medium">{{ serverInfo?.cpu || '未知' }}</div>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-600">内存</div>
<div class="font-medium">{{ serverInfo?.memory || '未知' }}GB</div>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-600">系统盘</div>
<div class="font-medium">{{ serverInfo?.disk_size || '未知' }}GB</div>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-600">带宽</div>
<div class="font-medium">{{ serverInfo?.bandwidth || '未知' }}Mbps</div>
</div>
</div>
<div class="mt-3 text-sm text-gray-600">
当前月费¥{{ serverInfo?.plan_price || '0' }}
</div>
</div>
<!-- 可升级套餐列表 -->
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">选择升级套餐</h3>
<div v-if="upgradePlansLoading" class="text-center py-8">
<div class="h-8 w-8 mx-auto animate-spin text-blue-600 mb-4 flex items-center justify-center">
<i class="fe fe-loader text-2xl"></i>
</div>
<p class="text-gray-700">正在加载可升级套餐...</p>
</div>
<div v-else-if="upgradePlans.length === 0" class="text-center py-8">
<p class="text-gray-500">暂无可升级套餐</p>
</div>
<div v-else class="space-y-3">
<div
v-for="plan in upgradePlans"
:key="plan.plan_id"
@click="selectedPlan = plan"
:class="[
'p-4 border rounded-lg cursor-pointer transition-all',
selectedPlan?.plan_id === plan.plan_id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
]"
>
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="font-medium text-gray-900 mb-2">
{{ plan.core }} {{ plan.memory }}GB {{ plan.disk_size }}GB
</div>
<div class="grid grid-cols-2 gap-2 text-sm text-gray-600">
<div>CPU: {{ plan.core }}</div>
<div>内存: {{ plan.memory }}GB</div>
<div>系统盘: {{ plan.disk_size }}GB</div>
<div>带宽: {{ plan.bandwidth }}Mbps</div>
</div>
</div>
<div class="text-right">
<div class="text-lg font-bold text-blue-600">
¥{{ plan.origin_price }}/
</div>
<div v-if="getPriceDiff(plan) > 0" class="text-sm text-green-600">
+¥{{ getPriceDiff(plan) }}/
</div>
<div v-else-if="getPriceDiff(plan) < 0" class="text-sm text-orange-600">
-¥{{ Math.abs(getPriceDiff(plan)) }}/
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 升级费用计算 -->
<div v-if="selectedPlan && upgradePriceInfo" class="mb-6 border-t border-gray-200 pt-4">
<h3 class="text-lg font-medium text-gray-900 mb-4">费用明细</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">当前套餐月费</span>
<span>¥{{ upgradePriceInfo.current_plan_price }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">新套餐月费</span>
<span>¥{{ upgradePriceInfo.new_plan_price }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">剩余天数</span>
<span>{{ upgradePriceInfo.remaining_days }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">每日差价</span>
<span>¥{{ upgradePriceInfo.daily_price_diff.toFixed(2) }}</span>
</div>
<div class="border-t pt-2">
<div class="flex justify-between text-lg font-bold">
<span>升级费用</span>
<span class="text-blue-600">¥{{ upgradePriceInfo.upgrade_amount.toFixed(2) }}</span>
</div>
</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-2">
您的服务器升级已提交请3至5分钟后刷新页面查看结果...
</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"
@load="qrcodeLoaded = true"
/>
</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="createUpgradeOrder"
:disabled="isLoading || !selectedPlan || !selectedPaymentMethod"
>
{{ 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="cancel"
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: 'JsiteServerUpgradeDialog',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
props: {
server: {
type: String,
required: true
},
serverDoc: {
type: Object,
default: null
}
},
emits: ['success'],
data() {
return {
show: true,
selectedPlan: null,
selectedPaymentMethod: null,
order: null,
showPaymentProcessing: false,
paymentSuccess: false,
error: null,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null,
upgradePlans: [],
upgradePlansLoading: false,
upgradeInfo: null
};
},
computed: {
serverInfo() {
if (!this.serverDoc) return null;
return {
cpu: this.serverDoc.cpu,
memory: this.serverDoc.memory,
disk_size: this.serverDoc.disk_size,
bandwidth: this.serverDoc.bandwidth,
plan_price: this.serverDoc.plan_price || 0,
instance_id: this.serverDoc.instance_id,
region: this.serverDoc.region
};
},
upgradePriceInfo() {
if (!this.selectedPlan || !this.serverInfo) return null;
const currentPrice = this.serverInfo.plan_price;
const newPrice = parseFloat(this.selectedPlan.origin_price);
const priceDiff = newPrice - currentPrice;
// 假设剩余30天这里应该从服务器信息获取实际剩余天数
const remainingDays = 30;
const dailyPriceDiff = priceDiff / 30;
const upgradeAmount = dailyPriceDiff * remainingDays;
return {
current_plan_price: currentPrice,
new_plan_price: newPrice,
remaining_days: remainingDays,
daily_price_diff: dailyPriceDiff,
upgrade_amount: upgradeAmount
};
},
isLoading() {
return this.$resources.createUpgradeOrder.loading;
}
},
async mounted() {
await this.loadUpgradePlans();
},
resources: {
loadUpgradePlans() {
return {
url: 'jcloud.api.aliyun_server_light.get_aliyun_instance_upgrade_plans',
params: {
instance_id: this.serverInfo?.instance_id,
region_id: this.serverInfo?.region
},
onSuccess(data) {
if (data.success && data.data && data.data.plans) {
this.upgradePlans = data.data.plans;
} else {
this.error = data.message || '获取可升级套餐失败';
}
},
onError(error) {
this.error = error.message || '获取可升级套餐失败';
}
};
},
createUpgradeOrder() {
return {
url: 'jcloud.api.aliyun_server_light.create_server_upgrade_order',
validate() {
if (!this.server) {
throw new DashboardError('缺少服务器信息');
}
if (!this.selectedPlan) {
throw new DashboardError('请选择升级套餐');
}
},
onSuccess(data) {
if (!data.success) {
this.error = data.message || '创建升级订单失败';
return;
}
// 保存升级信息
this.upgradeInfo = data.upgrade_info;
// 显示订单支付界面
this.order = data.order;
this.showPaymentProcessing = true;
// 立即处理支付
this.processPayment();
},
onError(error) {
this.error = error.message || '创建升级订单失败';
}
};
},
processBalancePayment() {
return {
url: 'jcloud.api.billing.process_balance_payment_for_server_order',
params: {},
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;
}
// 支付成功,等待支付完成回调处理升级
this.$emit('success', {
order: this.order,
selected_plan: this.selectedPlan,
message: response.message || '支付成功'
});
this.isProcessingPayment = false;
this.paymentSuccess = true;
},
onError(error) {
this.error = error.message || '余额支付处理失败';
this.isProcessingPayment = false;
}
};
},
processAlipayPayment() {
return {
url: 'jcloud.api.billing.process_alipay_order',
params: {},
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',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
this.paymentQrCode = response.payment_url;
this.paymentQrCodeImage = response.qr_code_image;
toast.success('请使用微信扫码支付');
// 开始轮询支付状态
this.startPaymentCheck();
},
onError(error) {
this.error = error.message || '微信支付处理失败';
this.isProcessingPayment = false;
}
};
},
checkPaymentStatus() {
return {
url: 'jcloud.api.billing.check_order_payment_status',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(data) {
if (data && data.status === '已支付') {
// 支付成功,停止轮询
this.stopPaymentCheck();
// 支付成功,等待支付完成回调处理升级
this.$emit('success', {
order: this.order,
selected_plan: this.selectedPlan,
message: '支付成功'
});
this.paymentSuccess = true;
}
},
onError(error) {
console.error('检查支付状态失败:', error);
}
};
}
},
methods: {
async loadUpgradePlans() {
this.upgradePlansLoading = true;
this.error = null;
try {
await this.$resources.loadUpgradePlans.submit();
} catch (error) {
this.error = error.message || '获取可升级套餐失败';
} finally {
this.upgradePlansLoading = false;
}
},
getPriceDiff(plan) {
if (!this.serverInfo) return 0;
const currentPrice = this.serverInfo.plan_price;
const newPrice = parseFloat(plan.origin_price);
return newPrice - currentPrice;
},
cancel() {
this.stopPaymentCheck();
this.show = false;
},
createUpgradeOrder() {
if (!this.selectedPaymentMethod) {
this.error = '请选择支付方式';
return;
}
this.error = null;
this.$resources.createUpgradeOrder.submit({
server: this.server,
new_plan_id: this.selectedPlan.plan_id
});
},
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
});
}
},
startPaymentCheck() {
this.isProcessingPayment = false;
this.checkInterval = setInterval(() => {
this.$resources.checkPaymentStatus.submit({
order_id: this.order.order_id
});
}, 3000);
// 15分钟后停止检查
setTimeout(() => {
this.stopPaymentCheck();
}, 900000);
},
stopPaymentCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
},
beforeUnmount() {
this.stopPaymentCheck();
}
};
</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);
}
}
.qrcode-container {
animation: fadeIn 0.3s ease-out;
}
</style>