jcloud/dashboard/src2/components/JsiteDomainRenewalDialog.vue

537 lines
17 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">
域名 <span class="font-medium">{{ domainDoc?.domain }}</span>
</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.years"
@click="onRenewalPeriodChange(period.years)"
:class="[
'px-4 py-2 text-sm font-medium rounded-md border',
renewalPeriod === period.years
? '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 class="mb-6 border-t border-gray-200 pt-4">
<div v-if="renewPrice" class="flex justify-between items-center">
<div class="text-sm text-gray-600">续费时长</div>
<div class="font-medium">{{ renewalPeriod }} </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 v-if="renewPrice" class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div>¥ {{ totalAmount }}</div>
</div>
<div v-else class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div class="text-gray-400">获取价格中...</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">
您的域名已成功续费 {{ renewalPeriod }}
</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 disabled:bg-gray-300 disabled:cursor-not-allowed"
@click="createRenewalOrder"
:disabled="isLoading || isPriceLoading || !renewPrice || !selectedPaymentMethod"
>
{{ isLoading ? '处理中...' : (isPriceLoading ? '获取价格中...' : '确认续费') }}
</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: 'JsiteDomainRenewalDialog',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
props: {
domain: {
type: String,
required: true
},
domainDoc: {
type: Object,
default: null
}
},
emits: ['success'],
data() {
return {
show: true,
renewalPeriod: 1, // 续费周期默认选择1年
selectedPaymentMethod: null, // 默认选择余额支付
order: null,
showPaymentProcessing: false,
paymentSuccess: false,
error: null,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null,
domainPrice: null,
renewPrice: null, // 当前选择年限的总价
renewalPeriods: [
{ years: 1, name: '1年', discount: 0 },
{ years: 2, name: '2年', discount: 0 },
{ years: 3, name: '3年', discount: 0 },
{ years: 5, name: '5年', discount: 0 },
{ years: 10, name: '10年', discount: 0 }
]
};
},
computed: {
discountPercentage() {
const period = this.renewalPeriods.find(p => p.years === this.renewalPeriod);
return period ? period.discount : 0;
},
totalAmount() {
if (!this.renewPrice) return '0.00';
const discount = this.renewPrice * (this.discountPercentage / 100);
return (this.renewPrice - discount).toFixed(2);
},
isLoading() {
return this.$resources.createRenewalOrder.loading;
},
isPriceLoading() {
return this.$resources.getRenewPrice.loading;
}
},
resources: {
createRenewalOrder() {
return {
url: 'jcloud.api.domain_west.create_domain_renew_order',
validate() {
if (!this.domain) {
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_domain_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,
renewal_period: this.renewalPeriod,
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 || null;
// 开始轮询支付状态
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,
renewal_period: this.renewalPeriod,
message: '支付成功'
});
this.paymentSuccess = true;
}
}
};
},
getRenewPrice() {
return {
url: 'jcloud.api.domain_west.get_west_domain_renew_price',
params: {
domain: this.domainDoc?.domain,
year: 1
},
onSuccess(data) {
if (data.data?.price) {
this.renewPrice = data.data.price;
}
},
onError(error) {
console.warn('获取域名续费价格失败:', error.message);
}
};
}
},
async mounted() {
// 获取当前选择年限的续费价格
if (this.domainDoc?.domain) {
await this.$resources.getRenewPrice.submit({
domain: this.domainDoc.domain,
year: this.renewalPeriod
});
}
},
methods: {
async onRenewalPeriodChange(years) {
this.renewalPeriod = years;
// 立即获取对应年限的价格
if (this.domainDoc?.domain) {
await this.$resources.getRenewPrice.submit({
domain: this.domainDoc.domain,
year: years
});
}
},
cancel() {
this.stopPaymentCheck();
this.show = false;
},
createRenewalOrder() {
this.error = null;
this.$resources.createRenewalOrder.submit({
domain: this.domain,
renewal_years: this.renewalPeriod
});
},
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);
}
}
</style>