main #2
@ -292,27 +292,13 @@ export default {
|
||||
return regionMap[region] || region;
|
||||
},
|
||||
renewServer() {
|
||||
const renewRequest = createResource({
|
||||
url: '/api/action/jcloud.api.aliyun_server_light.renew_aliyun_instance',
|
||||
params: {
|
||||
instance_id: this.$jsiteServer.pg.instance_id,
|
||||
region_id: this.$jsiteServer.pg.region
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('续费请求已提交');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getToastErrorMessage(error));
|
||||
}
|
||||
});
|
||||
toast.promise(
|
||||
renewRequest.submit(),
|
||||
{
|
||||
loading: '正在处理续费请求...',
|
||||
success: '续费请求已提交',
|
||||
error: (e) => getToastErrorMessage(e),
|
||||
},
|
||||
);
|
||||
const JsiteServerRenewalDialog = defineAsyncComponent(() => import('./JsiteServerRenewalDialog.vue'));
|
||||
|
||||
renderDialog(h(JsiteServerRenewalDialog, {
|
||||
server: this.server,
|
||||
serverDoc: this.$jsiteServer.pg,
|
||||
onSuccess: this.onRenewalSuccess
|
||||
}));
|
||||
},
|
||||
async restartServer() {
|
||||
if (!this.$jsiteServer.pg.instance_id) {
|
||||
@ -501,6 +487,11 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
onRenewalSuccess(data) {
|
||||
toast.success('服务器续费成功!');
|
||||
// 刷新服务器数据
|
||||
this.$jsiteServer.reload();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
serverInformation() {
|
||||
|
||||
541
dashboard/src2/components/JsiteServerRenewalDialog.vue
Normal file
541
dashboard/src2/components/JsiteServerRenewalDialog.vue
Normal file
@ -0,0 +1,541 @@
|
||||
<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>
|
||||
Loading…
x
Reference in New Issue
Block a user