jcloud/dashboard/src2/components/JsiteServerRenewalDialog.vue

541 lines
18 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">
<label class="block text-sm font-medium text-gray-700 mb-2">
续费周期
</label>
<div class="flex flex-wrap gap-3">
<button
v-for="period in renewalPeriods"
:key="period.months"
@click="selectedPeriod = period.months"
:class="[
'px-4 py-2 text-sm font-medium rounded-md border',
selectedPeriod === period.months
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
]"
>
{{ period.name }}
<span v-if="period.discount" class="ml-1 text-green-600">-{{ period.discount }}%</span>
</button>
</div>
</div>
<div v-if="serverInfo" 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">
¥ {{ serverInfo.plan_price }}
<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">{{ selectedPeriod }} 个月</div>
</div>
<div v-if="discountPercentage > 0" class="flex justify-between items-center mt-2">
<div class="text-sm text-green-600">折扣</div>
<div class="font-medium text-green-600">-{{ discountPercentage }}%</div>
</div>
<div class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div>¥ {{ totalAmount }}</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">
您的服务器已成功续费 {{ selectedPeriod }} 个月
</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="createRenewalOrder"
: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="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: 'JsiteServerRenewalDialog',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
props: {
server: {
type: String,
required: true
},
serverDoc: {
type: Object,
default: null
}
},
emits: ['success'],
data() {
return {
show: true,
selectedPeriod: 12, // 续费周期默认选择12个月
selectedPaymentMethod: null, // 默认选择余额支付
order: null,
showPaymentProcessing: false,
paymentSuccess: false,
error: null,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null,
renewalPeriods: [
{ months: 1, name: '1个月', discount: 0 },
{ months: 3, name: '3个月', discount: 0 },
{ months: 6, name: '6个月', discount: 0 },
{ months: 12, name: '1年', discount: 0 },
{ months: 24, name: '2年', discount: 0 },
{ months: 36, name: '3年', discount: 0 }
]
};
},
computed: {
serverInfo() {
if (!this.serverDoc) return null;
return {
plan_price: this.serverDoc.plan_price || 0,
instance_id: this.serverDoc.instance_id,
region: this.serverDoc.region
};
},
discountPercentage() {
const period = this.renewalPeriods.find(p => p.months === this.selectedPeriod);
return period ? period.discount : 0;
},
totalAmount() {
if (!this.serverInfo) return '0';
const basePrice = this.serverInfo.plan_price * this.selectedPeriod;
const discount = basePrice * (this.discountPercentage / 100);
return (basePrice - discount).toFixed(2);
},
isLoading() {
return this.$resources.createRenewalOrder.loading;
}
},
resources: {
createRenewalOrder() {
return {
url: 'jcloud.api.billing.create_server_renewal_order',
validate() {
if (!this.server) {
throw new DashboardError('缺少服务器信息');
}
if (!this.selectedPaymentMethod) {
throw new DashboardError('请选择支付方式');
}
},
onSuccess(data) {
if (!data.success) {
this.error = data.message || '创建续费订单失败';
return;
}
// 显示订单支付界面
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_renew_order',
params: {
order_id: this.order?.order_id
},
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;
}
// 调用服务器续费API
this.$resources.renewServer.submit();
},
onError(error) {
this.error = error.message || '余额支付处理失败';
this.isProcessingPayment = false;
}
};
},
processAlipayPayment() {
return {
url: 'jcloud.api.billing.process_alipay_order',
params: {
order_id: this.order?.order_id
},
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: {
order_id: this.order?.order_id
},
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();
// 调用服务器续费API
this.$resources.renewServer.submit();
}
}
};
},
renewServer() {
return {
url: 'jcloud.api.aliyun_server_light.renew_aliyun_instance',
params: {
instance_id: this.serverInfo?.instance_id,
period: this.selectedPeriod,
region_id: this.serverInfo?.region
},
validate() {
if (!this.serverInfo?.instance_id || !this.serverInfo?.region) {
throw new DashboardError('缺少服务器信息');
}
},
onSuccess(response) {
if (response.message?.success) {
this.$emit('success', {
order: this.order,
renewal_period: this.selectedPeriod,
message: response.message.message
});
this.isProcessingPayment = false;
this.paymentSuccess = true;
} else {
this.error = response.message?.message || '续费失败';
this.isProcessingPayment = false;
}
},
onError(error) {
this.error = error.message || '服务器续费失败';
this.isProcessingPayment = false;
}
};
}
},
methods: {
cancel() {
this.stopPaymentCheck();
this.show = false;
},
createRenewalOrder() {
if (!this.selectedPaymentMethod) {
this.error = '请选择支付方式';
return;
}
this.error = null;
this.$resources.createRenewalOrder.submit({
server: this.server,
renewal_months: this.selectedPeriod,
payment_method: this.selectedPaymentMethod
});
},
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();
} else if (this.selectedPaymentMethod === 'alipay') {
this.$resources.processAlipayPayment.submit();
} else if (this.selectedPaymentMethod === 'wechatpay') {
this.$resources.processWechatPayment.submit();
}
},
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;
}
}
},
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);
}
}
</style>