jcloud/dashboard/src2/components/SiteRenewalDialog.vue

562 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="planInfo" class="mb-6 border-t border-gray-200 pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-600">{{ planInfo.interval === 'Annually' ? '年度费用' : '月度费用' }}</div>
<div class="font-medium">
{{ planInfo.currency }} {{ planInfo.price }}
<span class="text-gray-500 text-sm">({{ planInfo.interval === 'Annually' ? '年付' : '月付' }})</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>{{ planInfo.currency }} {{ 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 { getCachedDocumentResource, Dialog } from 'jingrow-ui';
import { DashboardError } from '../utils/error';
import AlipayLogo from '../logo/AlipayLogo.vue';
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
export default {
name: 'SiteRenewalDialog',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
props: {
site: {
type: String,
required: true
},
siteDoc: {
type: Object,
default: null
}
},
emits: ['success'],
data() {
return {
show: true,
selectedPeriod: 12, // 续费周期默认选择12个月
selectedPaymentMethod: null, // 默认选择余额支付
order: null,
showPaymentProcessing: false,
paymentSuccess: false,
updatedExpiryDate: null,
error: null,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null,
renewalPeriods: [
{ months: 1, name: '1个月', discount: 0 },
{ months: 12, name: '1年', discount: 10 },
{ months: 36, name: '3年', discount: 15 },
{ months: 60, name: '5年', discount: 20 }
]
};
},
computed: {
planInfo() {
if (!this.$site?.pg?.current_plan) return null;
const currency = this.$team.pg.currency;
return {
price: currency === 'CNY'
? this.$site.pg.current_plan.price_cny
: this.$site.pg.current_plan.price_usd,
currency: currency === 'CNY' ? '¥' : '$',
interval: this.$site.pg.current_plan.interval || 'Monthly'
};
},
discountPercentage() {
const period = this.renewalPeriods.find(p => p.months === this.selectedPeriod);
return period ? period.discount : 0;
},
totalAmount() {
if (!this.planInfo) return '0';
let basePrice;
if (this.planInfo.interval === 'Annually') {
const monthlyPrice = this.planInfo.price / 12;
basePrice = monthlyPrice * this.selectedPeriod;
} else {
basePrice = this.planInfo.price * this.selectedPeriod;
}
const discount = basePrice * (this.discountPercentage / 100);
return (basePrice - discount).toFixed(2);
},
$site() {
return getCachedDocumentResource('Site', this.site);
},
$team() {
return this.$site?.pg?.team
? getCachedDocumentResource('Team', this.$site.pg.team)
: null;
},
isLoading() {
return this.$resources.createRenewalOrder.loading;
}
},
resources: {
createRenewalOrder() {
return {
url: 'jcloud.api.billing.create_renewal_order',
params: {
// 移除这里的固定参数定义改为通过submit时传入
},
validate() {
if (!this.site) {
throw new DashboardError('缺少站点信息');
}
},
onSuccess(data) {
if (!data.success) {
this.error = data.message || '创建续费订单失败';
return;
}
// 显示订单支付界面
this.order = data.order;
this.showPaymentProcessing = true;
// 立即处理支付
this.processPayment();
},
onError(error) {
console.error('创建续费订单失败:', error);
this.error = error.message || '创建续费订单失败';
}
};
},
processBalancePayment() {
return {
url: 'jcloud.api.billing.process_balance_payment_for_renew_order',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
// 检查响应中的success字段
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: '已支付'
};
this.$emit('success', orderData);
// 更新UI状态显示成功信息不再显示toast
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: {
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提示
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();
// 移除这里的toast提示避免重复
// toast.success('支付成功!');
// 确保传递完整的订单信息,包括原始订单和服务器返回的数据
const orderData = { ...this.order, ...(data.order || {}) };
this.$emit('success', orderData);
// 不再关闭弹窗,而是显示成功信息
this.paymentSuccess = true;
}
}
};
}
},
methods: {
cancel() {
this.stopPaymentCheck();
this.show = false;
},
createRenewalOrder() {
if (!this.selectedPaymentMethod) {
this.error = '请选择支付方式';
return;
}
this.error = null;
this.$resources.createRenewalOrder.submit({
site: this.site,
renewal_months: this.selectedPeriod
});
},
processPayment() {
this.isProcessingPayment = true;
this.error = null;
if (!this.order || !this.order.order_id) {
console.error('缺少订单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();
}, 3000);
// 15分钟后停止检查
setTimeout(() => {
this.stopPaymentCheck();
}, 900000);
},
stopPaymentCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
},
backToSelection() {
this.stopPaymentCheck();
this.showPaymentProcessing = false;
this.paymentUrl = null;
this.paymentQrCode = null;
this.paymentQrCodeImage = null;
this.error = 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>