dashboard左侧增加域名菜单及实现相关功能
This commit is contained in:
parent
f6b2e9dd31
commit
7046ab9850
417
dashboard/src2/components/JsiteDomainOverview.vue
Normal file
417
dashboard/src2/components/JsiteDomainOverview.vue
Normal file
@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="$domain?.pg"
|
||||
class="grid grid-cols-1 items-start gap-5 lg:grid-cols-2"
|
||||
>
|
||||
<!-- 左侧区块:基本信息 -->
|
||||
<div class="col-span-1 space-y-5">
|
||||
<!-- 当前套餐卡片 -->
|
||||
<div class="rounded-md border">
|
||||
<div class="h-12 border-b px-5 py-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">域名信息</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="flex h-full flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="mb-4 sm:mb-0">
|
||||
<div v-if="$domain.pg.price" class="text-lg font-bold text-green-600">
|
||||
¥{{ $domain.pg.price }}/年
|
||||
</div>
|
||||
|
||||
<div v-if="$domain.pg.end_date" class="mt-2 inline-flex items-center rounded-full bg-amber-50 px-4 py-2 text-sm font-medium text-amber-800">
|
||||
<ClockIcon class="mr-1.5 h-4 w-4 text-amber-500" />
|
||||
到期时间:{{ $format.date($domain.pg.end_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
@click="renewDomain"
|
||||
:loading="$domain.renew?.loading"
|
||||
class="px-5 !bg-[#1fc76f] !hover:bg-[#1bb85f] !text-white"
|
||||
>
|
||||
续费
|
||||
</Button>
|
||||
<Button
|
||||
@click="transferDomain"
|
||||
:loading="transferLoading"
|
||||
class="px-5 !bg-[#3b82f6] !hover:bg-[#2563eb] !text-white"
|
||||
>
|
||||
转入
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名配置 -->
|
||||
<div class="rounded-md border">
|
||||
<div class="h-12 border-b px-5 py-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">域名配置</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">域名:</span>
|
||||
<span class="text-lg font-semibold text-blue-600">{{ $domain.pg.domain || '未知' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">注册商:</span>
|
||||
<span class="text-lg font-semibold text-green-600">{{ $domain.pg.domain_registrar || '未知' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">所有者:</span>
|
||||
<span class="text-lg font-semibold text-purple-600">{{ $domain.pg.domain_owner || '未知' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">购买时长:</span>
|
||||
<span class="text-lg font-semibold text-orange-600">{{ $domain.pg.period || '未知' }}年</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名信息 -->
|
||||
<div class="rounded-md border">
|
||||
<div class="h-12 border-b px-5 py-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">域名信息</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-for="info in domainInformation"
|
||||
:key="info.label"
|
||||
class="flex items-center px-5 py-3 last:pb-5 even:bg-gray-50/70"
|
||||
>
|
||||
<div class="w-1/3 text-base text-gray-600">{{ info.label }}</div>
|
||||
<div
|
||||
class="flex w-2/3 items-center space-x-2 text-base text-gray-900"
|
||||
>
|
||||
<div v-if="info.prefix">
|
||||
<component :is="info.prefix" />
|
||||
</div>
|
||||
<span>
|
||||
{{ info.value }}
|
||||
</span>
|
||||
<div v-if="info.suffix">
|
||||
<component :is="info.suffix" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区块:DNS信息和操作 -->
|
||||
<div class="col-span-1 space-y-5">
|
||||
<!-- 操作 -->
|
||||
<div class="rounded-md border">
|
||||
<div class="h-12 border-b px-5 py-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">操作</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
@click="manageDNS"
|
||||
:loading="dnsLoading"
|
||||
variant="outline"
|
||||
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
管理DNS
|
||||
</Button>
|
||||
<Button
|
||||
@click="toggleAutoRenew"
|
||||
:loading="autoRenewLoading"
|
||||
variant="outline"
|
||||
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{{ $domain.pg.auto_renew ? '关闭自动续费' : '开启自动续费' }}
|
||||
</Button>
|
||||
<Button
|
||||
@click="toggleWhoisProtection"
|
||||
:loading="whoisProtectionLoading"
|
||||
variant="outline"
|
||||
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{{ $domain.pg.whois_protection ? '关闭隐私保护' : '开启隐私保护' }}
|
||||
</Button>
|
||||
<Button
|
||||
@click="deleteDomain"
|
||||
:loading="deleteLoading"
|
||||
variant="outline"
|
||||
class="bg-red-50 text-red-700 hover:bg-red-100 border-red-200"
|
||||
>
|
||||
删除域名
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS服务器信息 -->
|
||||
<div class="rounded-md border">
|
||||
<div class="h-12 border-b px-5 py-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">DNS服务器</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">主DNS:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host1 || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">辅DNS:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host2 || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">DNS3:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host3 || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">DNS4:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host4 || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">DNS5:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host5 || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">DNS6:</span>
|
||||
<span class="font-mono text-gray-900">{{ $domain.pg.dns_host6 || '未设置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCachedDocumentResource, Badge, Tooltip, Button, createResource } from 'jingrow-ui';
|
||||
import { h, defineAsyncComponent } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { getToastErrorMessage } from '../utils/toast';
|
||||
import { renderDialog, confirmDialog } from '../utils/components';
|
||||
import ClockIcon from '~icons/lucide/clock';
|
||||
import InfoIcon from '~icons/lucide/info';
|
||||
|
||||
export default {
|
||||
name: 'JsiteDomainOverview',
|
||||
props: ['domain'],
|
||||
components: {
|
||||
Badge,
|
||||
Button,
|
||||
ClockIcon,
|
||||
InfoIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
renewLoading: false,
|
||||
transferLoading: false,
|
||||
dnsLoading: false,
|
||||
autoRenewLoading: false,
|
||||
whoisProtectionLoading: false,
|
||||
deleteLoading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'Pending': '待处理',
|
||||
'Active': '活跃',
|
||||
'Expired': '已过期',
|
||||
'Suspended': '已暂停',
|
||||
'Cancelled': '已取消'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
},
|
||||
getStatusVariant(status) {
|
||||
const variantMap = {
|
||||
'Pending': 'warning',
|
||||
'Active': 'success',
|
||||
'Expired': 'danger',
|
||||
'Suspended': 'danger',
|
||||
'Cancelled': 'danger'
|
||||
};
|
||||
return variantMap[status] || 'default';
|
||||
},
|
||||
renewDomain() {
|
||||
const JsiteDomainRenewalDialog = defineAsyncComponent(() => import('./JsiteDomainRenewalDialog.vue'));
|
||||
|
||||
renderDialog(h(JsiteDomainRenewalDialog, {
|
||||
domain: this.domain,
|
||||
domainDoc: this.$domain.pg,
|
||||
onSuccess: this.onRenewalSuccess
|
||||
}));
|
||||
},
|
||||
transferDomain() {
|
||||
const JsiteDomainTransferDialog = defineAsyncComponent(() => import('./JsiteDomainTransferDialog.vue'));
|
||||
|
||||
renderDialog(h(JsiteDomainTransferDialog, {
|
||||
domain: this.domain,
|
||||
domainDoc: this.$domain.pg,
|
||||
onSuccess: this.onTransferSuccess
|
||||
}));
|
||||
},
|
||||
async manageDNS() {
|
||||
if (!this.$domain.pg.domain) {
|
||||
toast.error('域名信息不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里可以打开DNS管理对话框
|
||||
toast.info('DNS管理功能开发中...');
|
||||
},
|
||||
async toggleAutoRenew() {
|
||||
if (!this.$domain.pg.name) {
|
||||
toast.error('域名记录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = !this.$domain.pg.auto_renew;
|
||||
const actionText = newValue ? '开启' : '关闭';
|
||||
|
||||
confirmDialog({
|
||||
title: `${actionText}自动续费`,
|
||||
message: `确定要${actionText}域名 "${this.$domain.pg.domain}" 的自动续费吗?`,
|
||||
primaryAction: {
|
||||
label: '确定',
|
||||
onClick: ({ hide }) => {
|
||||
toast.success(`${actionText}自动续费请求已提交`);
|
||||
hide();
|
||||
|
||||
this.autoRenewLoading = true;
|
||||
const toggleRequest = createResource({
|
||||
url: '/api/action/jcloud.api.domain_west.toggle_domain_auto_renew',
|
||||
params: {
|
||||
pagetype: 'Jsite Domain',
|
||||
name: this.$domain.pg.name,
|
||||
auto_renew: newValue
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.autoRenewLoading = false;
|
||||
this.$domain.reload();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getToastErrorMessage(error));
|
||||
this.autoRenewLoading = false;
|
||||
}
|
||||
});
|
||||
toggleRequest.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
async toggleWhoisProtection() {
|
||||
if (!this.$domain.pg.name) {
|
||||
toast.error('域名记录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = !this.$domain.pg.whois_protection;
|
||||
const actionText = newValue ? '开启' : '关闭';
|
||||
|
||||
confirmDialog({
|
||||
title: `${actionText}隐私保护`,
|
||||
message: `确定要${actionText}域名 "${this.$domain.pg.domain}" 的隐私保护吗?`,
|
||||
primaryAction: {
|
||||
label: '确定',
|
||||
onClick: ({ hide }) => {
|
||||
toast.success(`${actionText}隐私保护请求已提交`);
|
||||
hide();
|
||||
|
||||
this.whoisProtectionLoading = true;
|
||||
const toggleRequest = createResource({
|
||||
url: '/api/action/jcloud.api.domain_west.toggle_domain_whois_protection',
|
||||
params: {
|
||||
pagetype: 'Jsite Domain',
|
||||
name: this.$domain.pg.name,
|
||||
whois_protection: newValue
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.whoisProtectionLoading = false;
|
||||
this.$domain.reload();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getToastErrorMessage(error));
|
||||
this.whoisProtectionLoading = false;
|
||||
}
|
||||
});
|
||||
toggleRequest.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
async deleteDomain() {
|
||||
if (!this.$domain.pg.name) {
|
||||
toast.error('域名记录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
confirmDialog({
|
||||
title: '删除域名',
|
||||
message: `确定要删除域名 "${this.$domain.pg.domain}" 吗?此操作不可逆!`,
|
||||
primaryAction: {
|
||||
label: '确定删除',
|
||||
onClick: ({ hide }) => {
|
||||
toast.success('删除域名请求已提交');
|
||||
hide();
|
||||
|
||||
this.deleteLoading = true;
|
||||
const deleteRequest = createResource({
|
||||
url: '/api/action/jcloud.api.domain_west.delete_domain',
|
||||
params: {
|
||||
pagetype: 'Jsite Domain',
|
||||
name: this.$domain.pg.name
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.deleteLoading = false;
|
||||
toast.success('域名删除成功');
|
||||
this.$router.push('/domains');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getToastErrorMessage(error));
|
||||
this.deleteLoading = false;
|
||||
}
|
||||
});
|
||||
deleteRequest.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
onRenewalSuccess(data) {
|
||||
toast.success('域名续费成功!');
|
||||
this.$domain.reload();
|
||||
},
|
||||
onTransferSuccess(data) {
|
||||
toast.success('域名转入成功!');
|
||||
this.$domain.reload();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
domainInformation() {
|
||||
return [
|
||||
{
|
||||
label: '状态',
|
||||
value: this.getStatusText(this.$domain.pg?.status),
|
||||
},
|
||||
{
|
||||
label: '域名',
|
||||
value: this.$domain.pg?.domain || this.$domain.pg?.name,
|
||||
},
|
||||
{
|
||||
label: '注册时间',
|
||||
value: this.$domain.pg?.registration_date || '未知',
|
||||
},
|
||||
{
|
||||
label: '注册商',
|
||||
value: this.$domain.pg?.domain_registrar || '未知',
|
||||
},
|
||||
{
|
||||
label: '所有者',
|
||||
value: this.$domain.pg?.domain_owner || '未知',
|
||||
},
|
||||
];
|
||||
},
|
||||
$domain() {
|
||||
return getCachedDocumentResource('Jsite Domain', this.domain);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
222
dashboard/src2/components/JsiteDomainRenewalDialog.vue
Normal file
222
dashboard/src2/components/JsiteDomainRenewalDialog.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">域名续费</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
为域名 <span class="font-medium">{{ domainDoc?.domain }}</span> 续费
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">续费时长</label>
|
||||
<select v-model="renewalPeriod" class="w-full border rounded px-3 py-2">
|
||||
<option v-for="period in renewalPeriods" :key="period.value" :value="period.value">
|
||||
{{ period.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="domainPrice" class="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">
|
||||
¥ {{ domainPrice }}
|
||||
<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">{{ renewalPeriod }} 年</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2 text-lg font-bold">
|
||||
<div>总计</div>
|
||||
<div>¥ {{ getTotalAmount() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="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">
|
||||
<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="p-3 bg-red-50 text-red-700 rounded-md text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="processRenewal"
|
||||
:disabled="!selectedPaymentMethod || isLoading"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 transition-colors"
|
||||
>
|
||||
{{ isLoading ? '处理中...' : '确认续费' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toast } from 'vue-sonner';
|
||||
import AlipayLogo from '../logo/AlipayLogo.vue';
|
||||
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
|
||||
|
||||
export default {
|
||||
name: 'JsiteDomainRenewalDialog',
|
||||
components: {
|
||||
AlipayLogo,
|
||||
WeChatPayLogo
|
||||
},
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
domainDoc: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
renewalPeriod: 1,
|
||||
selectedPaymentMethod: null,
|
||||
domainPrice: null,
|
||||
renewalPeriods: [
|
||||
{ value: 1, label: '1年' },
|
||||
{ value: 2, label: '2年' },
|
||||
{ value: 3, label: '3年' },
|
||||
{ value: 5, label: '5年' },
|
||||
{ value: 10, label: '10年' }
|
||||
],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getDomainPrice();
|
||||
},
|
||||
methods: {
|
||||
async getDomainPrice() {
|
||||
try {
|
||||
const response = await this.$resources.getDomainPrice.submit({
|
||||
domain: this.domainDoc.domain,
|
||||
year: 1
|
||||
});
|
||||
|
||||
if (response && response.data && response.data.price) {
|
||||
this.domainPrice = response.data.price;
|
||||
} else {
|
||||
this.domainPrice = this.domainDoc.price || 50;
|
||||
}
|
||||
} catch (error) {
|
||||
this.domainPrice = this.domainDoc.price || 50;
|
||||
}
|
||||
},
|
||||
getTotalAmount() {
|
||||
const yearlyPrice = this.domainPrice || 0;
|
||||
return (yearlyPrice * this.renewalPeriod).toFixed(2);
|
||||
},
|
||||
async processRenewal() {
|
||||
if (!this.selectedPaymentMethod) {
|
||||
this.error = '请选择支付方式';
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await this.$resources.renewDomain.submit({
|
||||
domain: this.domainDoc.domain,
|
||||
period: this.renewalPeriod,
|
||||
payment_method: this.selectedPaymentMethod
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success('续费请求已提交');
|
||||
this.$emit('close');
|
||||
this.onSuccess(response);
|
||||
} else {
|
||||
this.error = response.message || '续费失败';
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = error.message || '续费失败';
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
getDomainPrice() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.get_west_domain_price',
|
||||
onSuccess(response) {
|
||||
if (response && response.data && response.data.price) {
|
||||
this.domainPrice = response.data.price;
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
this.domainPrice = this.domainDoc.price || 50;
|
||||
}
|
||||
};
|
||||
},
|
||||
renewDomain() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.west_domain_renew',
|
||||
onSuccess(response) {
|
||||
return response;
|
||||
},
|
||||
onError(error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
204
dashboard/src2/components/JsiteDomainTransferDialog.vue
Normal file
204
dashboard/src2/components/JsiteDomainTransferDialog.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">域名转入</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
将域名转入到我们的平台进行管理
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">域名</label>
|
||||
<input
|
||||
v-model="transferDomain"
|
||||
type="text"
|
||||
placeholder="请输入要转入的域名,如:example.com"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">转移授权码</label>
|
||||
<input
|
||||
v-model="authCode"
|
||||
type="text"
|
||||
placeholder="请输入从原注册商获取的转移授权码"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
转移授权码需要从原域名注册商处获取
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="transferPrice" class="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">
|
||||
¥ {{ transferPrice }}
|
||||
<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">1 年</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2 text-lg font-bold">
|
||||
<div>总计</div>
|
||||
<div>¥ {{ transferPrice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="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">
|
||||
<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="p-3 bg-red-50 text-red-700 rounded-md text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="processTransfer"
|
||||
:disabled="!transferDomain || !authCode || !selectedPaymentMethod || isLoading"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 transition-colors"
|
||||
>
|
||||
{{ isLoading ? '处理中...' : '确认转入' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toast } from 'vue-sonner';
|
||||
import AlipayLogo from '../logo/AlipayLogo.vue';
|
||||
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
|
||||
|
||||
export default {
|
||||
name: 'JsiteDomainTransferDialog',
|
||||
components: {
|
||||
AlipayLogo,
|
||||
WeChatPayLogo
|
||||
},
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
domainDoc: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
transferDomain: '',
|
||||
authCode: '',
|
||||
selectedPaymentMethod: null,
|
||||
transferPrice: 50, // 默认转入价格
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async processTransfer() {
|
||||
if (!this.transferDomain) {
|
||||
this.error = '请输入要转入的域名';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.authCode) {
|
||||
this.error = '请输入转移授权码';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedPaymentMethod) {
|
||||
this.error = '请选择支付方式';
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await this.$resources.transferDomain.submit({
|
||||
domain: this.transferDomain,
|
||||
auth_code: this.authCode,
|
||||
payment_method: this.selectedPaymentMethod
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success('域名转入请求已提交');
|
||||
this.$emit('close');
|
||||
this.onSuccess(response);
|
||||
} else {
|
||||
this.error = response.message || '转入失败';
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = error.message || '转入失败';
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
transferDomain() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.west_domain_transfer',
|
||||
onSuccess(response) {
|
||||
return response;
|
||||
},
|
||||
onError(error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -109,6 +109,15 @@ export default {
|
||||
condition: onboardingComplete && !isSaasUser && this.$team.pg.is_pro,
|
||||
disabled: enforce2FA,
|
||||
},
|
||||
{
|
||||
name: '域名',
|
||||
icon: () => h(Globe),
|
||||
route: '/domains',
|
||||
isActive:
|
||||
['Jsite Domain List', 'New Jsite Domain'].includes(routeName) ||
|
||||
routeName.startsWith('Jsite Domain'),
|
||||
disabled: enforce2FA,
|
||||
},
|
||||
{
|
||||
name: '服务器',
|
||||
icon: () => h(Server),
|
||||
|
||||
258
dashboard/src2/objects/domain.js
Normal file
258
dashboard/src2/objects/domain.js
Normal file
@ -0,0 +1,258 @@
|
||||
import { defineAsyncComponent, h } from 'vue';
|
||||
import LucideGlobe from '~icons/lucide/globe';
|
||||
import { getTeam } from '../data/team';
|
||||
import router from '../router';
|
||||
import { icon } from '../utils/components';
|
||||
import { duration, planTitle, userCurrency } from '../utils/format';
|
||||
import { trialDays } from '../utils/site';
|
||||
import { getJobsTab } from './common/jobs';
|
||||
import { tagTab } from './common/tags';
|
||||
|
||||
export default {
|
||||
pagetype: 'Jsite Domain',
|
||||
whitelistedMethods: {
|
||||
renew: 'renew',
|
||||
rename: 'rename',
|
||||
dropDomain: 'drop_domain',
|
||||
addTag: 'add_resource_tag',
|
||||
removeTag: 'remove_resource_tag'
|
||||
},
|
||||
list: {
|
||||
route: '/domains',
|
||||
title: '域名',
|
||||
fields: [
|
||||
'name',
|
||||
'domain',
|
||||
'status',
|
||||
'domain_owner',
|
||||
'domain_registrar',
|
||||
'registration_date',
|
||||
'end_date',
|
||||
'price',
|
||||
'period',
|
||||
'auto_renew',
|
||||
'team',
|
||||
'order_id',
|
||||
'description'
|
||||
],
|
||||
filterControls() {
|
||||
return [
|
||||
{
|
||||
type: 'select',
|
||||
label: '状态',
|
||||
fieldname: 'status',
|
||||
options: [
|
||||
{ label: '', value: '' },
|
||||
{ label: '待处理', value: 'Pending' },
|
||||
{ label: '活跃', value: 'Active' },
|
||||
{ label: '已过期', value: 'Expired' },
|
||||
{ label: '已暂停', value: 'Suspended' },
|
||||
{ label: '已取消', value: 'Cancelled' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '自动续费',
|
||||
fieldname: 'auto_renew',
|
||||
options: [
|
||||
{ label: '', value: '' },
|
||||
{ label: '是', value: '1' },
|
||||
{ label: '否', value: '0' }
|
||||
]
|
||||
}
|
||||
];
|
||||
},
|
||||
orderBy: 'creation desc',
|
||||
searchField: 'domain',
|
||||
columns: [
|
||||
{
|
||||
label: '域名',
|
||||
fieldname: 'domain',
|
||||
width: 2,
|
||||
class: 'font-medium',
|
||||
format(value) {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
fieldname: 'status',
|
||||
type: 'Badge',
|
||||
width: 0.8,
|
||||
format(value) {
|
||||
const statusMap = {
|
||||
'Pending': '待处理',
|
||||
'Active': '活跃',
|
||||
'Expired': '已过期',
|
||||
'Suspended': '已暂停',
|
||||
'Cancelled': '已取消'
|
||||
};
|
||||
return statusMap[value] || value;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '所有者',
|
||||
fieldname: 'domain_owner',
|
||||
format(value) {
|
||||
return value || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '注册商',
|
||||
fieldname: 'domain_registrar',
|
||||
format(value) {
|
||||
return value || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '注册时间',
|
||||
fieldname: 'registration_date',
|
||||
format(value) {
|
||||
if (!value) return '-';
|
||||
return value;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '到期时间',
|
||||
fieldname: 'end_date',
|
||||
format(value) {
|
||||
if (!value) return '-';
|
||||
return value;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '价格',
|
||||
fieldname: 'price',
|
||||
format(value) {
|
||||
if (!value) return '-';
|
||||
return `¥${value}/年`;
|
||||
}
|
||||
}
|
||||
],
|
||||
primaryAction({ listResource: domains }) {
|
||||
return {
|
||||
label: '新建域名',
|
||||
variant: 'solid',
|
||||
slots: {
|
||||
prefix: icon('plus')
|
||||
},
|
||||
onClick() {
|
||||
router.push('/domains/new');
|
||||
}
|
||||
};
|
||||
},
|
||||
statusBadge({ documentResource: domain }) {
|
||||
const status = domain.pg?.status;
|
||||
const statusMap = {
|
||||
'Pending': '待处理',
|
||||
'Active': '活跃',
|
||||
'Expired': '已过期',
|
||||
'Suspended': '已暂停',
|
||||
'Cancelled': '已取消'
|
||||
};
|
||||
return {
|
||||
label: statusMap[status] || status
|
||||
};
|
||||
},
|
||||
breadcrumbs({ documentResource: domain }) {
|
||||
return [
|
||||
{
|
||||
label: '域名',
|
||||
route: '/domains'
|
||||
},
|
||||
{
|
||||
label: domain.pg?.domain || domain.pg?.name,
|
||||
route: `/domains/${domain.pg?.name}`
|
||||
}
|
||||
];
|
||||
},
|
||||
actions({ documentResource: domain }) {
|
||||
if (!domain) return [];
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: '续费',
|
||||
icon: 'refresh-cw',
|
||||
onClick() {
|
||||
domain.renew.submit();
|
||||
},
|
||||
condition: () => domain.pg?.status === 'Active'
|
||||
},
|
||||
{
|
||||
label: '重命名',
|
||||
icon: 'edit-3',
|
||||
onClick() {
|
||||
domain.rename.submit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'trash-2',
|
||||
onClick() {
|
||||
domain.dropDomain.submit();
|
||||
},
|
||||
condition: () => domain.pg?.status !== 'Active'
|
||||
}
|
||||
];
|
||||
return actions.filter(action => !action.condition || action.condition());
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
route: '/domains/:name',
|
||||
title: '域名详细信息',
|
||||
tabs: [
|
||||
{
|
||||
label: '概览',
|
||||
route: '',
|
||||
type: 'Component',
|
||||
component: defineAsyncComponent(() => import('../components/JsiteDomainOverview.vue')),
|
||||
props: domain => {
|
||||
return { domain: domain.pg?.name };
|
||||
}
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
label: '基本信息',
|
||||
fields: [
|
||||
'domain',
|
||||
'status',
|
||||
'domain_owner',
|
||||
'domain_registrar',
|
||||
'registration_date',
|
||||
'end_date'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '价格信息',
|
||||
fields: [
|
||||
'price',
|
||||
'period',
|
||||
'auto_renew',
|
||||
'order_id'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'DNS设置',
|
||||
fields: [
|
||||
'dns_host1',
|
||||
'dns_host2',
|
||||
'dns_host3',
|
||||
'dns_host4',
|
||||
'dns_host5',
|
||||
'dns_host6'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '其他信息',
|
||||
fields: [
|
||||
'description',
|
||||
'whois_protection'
|
||||
]
|
||||
}
|
||||
],
|
||||
actions({ documentResource: domain }) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -5,6 +5,7 @@ import marketplace from './marketplace';
|
||||
import server from './server';
|
||||
import jsite_server from './jsite_server';
|
||||
import notification from './notification';
|
||||
import domain from './domain';
|
||||
|
||||
let objects = {
|
||||
Site: site,
|
||||
@ -13,6 +14,7 @@ let objects = {
|
||||
Marketplace: marketplace,
|
||||
Server: server,
|
||||
'Jsite Server': jsite_server,
|
||||
'Jsite Domain': domain,
|
||||
Notification: notification
|
||||
};
|
||||
|
||||
|
||||
952
dashboard/src2/pages/NewJsiteDomain.vue
Normal file
952
dashboard/src2/pages/NewJsiteDomain.vue
Normal file
@ -0,0 +1,952 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 页面头部 -->
|
||||
<div class="sticky top-0 z-10 shrink-0">
|
||||
<Header>
|
||||
<Breadcrumbs
|
||||
:items="[
|
||||
{ label: '域名', route: '/domains' },
|
||||
{ label: '新建域名', route: '/domains/new' }
|
||||
]"
|
||||
/>
|
||||
</Header>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="mx-auto max-w-7xl px-5">
|
||||
<!-- 第一步:选择域名和支付方式 -->
|
||||
<div v-if="!showPaymentProcessing" class="space-y-12 pb-[50vh] pt-12">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">新建域名</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
请输入要注册的域名,系统将自动查询可用性并为您注册。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">域名查询</label>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
v-model="domainName"
|
||||
type="text"
|
||||
placeholder="请输入域名关键词,如:example"
|
||||
class="flex-1 border rounded px-3 py-2"
|
||||
@keyup.enter="checkDomain"
|
||||
/>
|
||||
<button
|
||||
@click="checkDomain"
|
||||
:disabled="!domainName || isChecking"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 whitespace-nowrap"
|
||||
>
|
||||
{{ isChecking ? '查询中...' : '查询域名' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名后缀选择区域 -->
|
||||
<div class="border rounded-lg p-4 bg-gray-50">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">选择域名后缀</h3>
|
||||
|
||||
<!-- 搜索和排序 -->
|
||||
<div class="flex flex-col sm:flex-row gap-2 mb-4">
|
||||
<div class="search-container">
|
||||
<input
|
||||
v-model="suffixSearch"
|
||||
type="text"
|
||||
placeholder="搜索后缀"
|
||||
class="search-input"
|
||||
/>
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<select v-model="suffixSort" class="px-3 py-2 text-sm border rounded-md bg-white min-w-[120px]">
|
||||
<option value="default">默认排序</option>
|
||||
<option value="popular">热门优先</option>
|
||||
<option value="alphabetical">字母排序</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<div class="category-tabs overflow-x-auto">
|
||||
<div class="flex gap-2 min-w-max">
|
||||
<button
|
||||
v-for="category in suffixCategories"
|
||||
:key="category.key"
|
||||
@click="selectedCategory = category.key"
|
||||
class="category-tab whitespace-nowrap"
|
||||
:class="{ active: selectedCategory === category.key }"
|
||||
>
|
||||
{{ category.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名后缀网格 -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 gap-2 domain-suffix-grid">
|
||||
<button
|
||||
v-for="suffix in filteredSuffixes"
|
||||
:key="suffix.value"
|
||||
@click="selectedSuffix = suffix.value"
|
||||
class="domain-suffix-item p-3 text-center rounded-lg border transition-all duration-200"
|
||||
:class="selectedSuffix === suffix.value
|
||||
? 'border-blue-500 border-2 shadow-sm bg-blue-50'
|
||||
: 'bg-white border-gray-200 hover:border-blue-300 hover:bg-gray-50'"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<span class="text-sm font-medium" :class="selectedSuffix === suffix.value ? 'text-blue-700' : 'text-gray-800'">{{ suffix.value }}</span>
|
||||
<span v-if="suffix.hot" class="hot-tag">
|
||||
HOT
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="suffix.price" class="text-xs mt-1 text-gray-500">
|
||||
¥{{ suffix.price }}/年
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名查询结果 -->
|
||||
<div v-if="domainCheckResult" class="border rounded-lg p-4">
|
||||
<div v-if="domainCheckResult.available" class="text-green-600">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="font-medium">域名可用</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">{{ fullDomain }} 可以注册</p>
|
||||
<div v-if="domainPrice" class="mt-2 text-lg font-bold text-green-600">
|
||||
¥{{ domainPrice }}/年
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-red-600">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="font-medium">域名不可用</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">{{ fullDomain }} 已被注册</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="domainCheckResult && domainCheckResult.available">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">购买时长</label>
|
||||
<select v-model="period" class="w-full border rounded px-3 py-2">
|
||||
<option v-for="p in periods" :key="p.value" :value="p.value">{{ p.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 价格信息显示 -->
|
||||
<div v-if="domainCheckResult && domainCheckResult.available && domainPrice" class="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">
|
||||
¥ {{ domainPrice }}
|
||||
<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">{{ period }} 年</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2 text-lg font-bold">
|
||||
<div>总计</div>
|
||||
<div>¥ {{ getTotalAmount() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<div v-if="domainCheckResult && domainCheckResult.available" class="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="p-3 bg-red-50 text-red-700 rounded-md text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<div v-if="domainCheckResult && domainCheckResult.available" class="pt-4">
|
||||
<button
|
||||
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="registerDomain"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? '处理中...' : '注册域名' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二步:处理支付 -->
|
||||
<div v-else class="space-y-12 pb-[50vh] pt-12">
|
||||
<!-- 加载中状态 -->
|
||||
<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-4">
|
||||
您的订单已支付成功,域名记录已创建。域名正在后台注册中,注册需要 1-3 分钟,请耐心等待。
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm">
|
||||
您可以在域名列表中查看域名状态,注册完成后状态将更新为"活跃"。
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<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"
|
||||
@click="goToDomainList"
|
||||
>
|
||||
返回域名列表
|
||||
</button>
|
||||
</div>
|
||||
</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="p-3 bg-red-50 text-red-700 rounded-md text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toast } from 'vue-sonner';
|
||||
import { DashboardError } from '../utils/error';
|
||||
import AlipayLogo from '../logo/AlipayLogo.vue';
|
||||
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
|
||||
import router from '../router';
|
||||
|
||||
export default {
|
||||
name: 'NewJsiteDomain',
|
||||
components: {
|
||||
AlipayLogo,
|
||||
WeChatPayLogo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainName: '',
|
||||
selectedSuffix: '.com',
|
||||
period: 1,
|
||||
selectedPaymentMethod: null,
|
||||
periods: [
|
||||
{ value: 1, label: '1年' },
|
||||
{ value: 2, label: '2年' },
|
||||
{ value: 3, label: '3年' },
|
||||
{ value: 5, label: '5年' },
|
||||
{ value: 10, label: '10年' }
|
||||
],
|
||||
isLoading: false,
|
||||
isChecking: false,
|
||||
error: null,
|
||||
domainCheckResult: null,
|
||||
domainPrice: null,
|
||||
// 支付相关状态
|
||||
order: null,
|
||||
domain: null,
|
||||
showPaymentProcessing: false,
|
||||
paymentSuccess: false,
|
||||
isProcessingPayment: false,
|
||||
paymentUrl: null,
|
||||
paymentQrCode: null,
|
||||
paymentQrCodeImage: null,
|
||||
checkInterval: null,
|
||||
// 域名后缀相关
|
||||
suffixSearch: '',
|
||||
suffixSort: 'default',
|
||||
selectedCategory: 'all',
|
||||
suffixCategories: [
|
||||
{ key: 'all', label: '全部域名' },
|
||||
{ key: 'popular', label: '热门域名' },
|
||||
{ key: 'gtd', label: '通用顶级域名' },
|
||||
{ key: 'ntd', label: '新顶级域名' },
|
||||
{ key: 'country', label: '国家/地区域名' },
|
||||
{ key: 'cn', label: 'CN域名' },
|
||||
{ key: 'chinese', label: '中文域名' }
|
||||
],
|
||||
allSuffixes: [
|
||||
{ value: '.com', label: '.com', hot: true, price: 50, category: 'com' },
|
||||
{ value: '.cn', label: '.cn', hot: true, price: 50, category: 'cn' },
|
||||
{ value: '.net', label: '.net', hot: true, price: 50, category: 'net' },
|
||||
{ value: '.org', label: '.org', hot: true, price: 50, category: 'org' },
|
||||
{ value: '.top', label: '.top', hot: true, price: 50, category: 'top' },
|
||||
{ value: '.xyz', label: '.xyz', hot: true, price: 50, category: 'xyz' },
|
||||
{ value: '.vip', label: '.vip', hot: true, price: 50, category: 'vip' },
|
||||
{ value: '.site', label: '.site', hot: true, price: 50, category: 'site' },
|
||||
{ value: '.shop', label: '.shop', hot: true, price: 50, category: 'shop' },
|
||||
{ value: '.io', label: '.io', hot: true, price: 50, category: 'io' },
|
||||
{ value: '.ai', label: '.ai', hot: true, price: 50, category: 'ai' },
|
||||
{ value: '.me', label: '.me', hot: true, price: 50, category: 'me' },
|
||||
{ value: '.co', label: '.co', hot: true, price: 50, category: 'co' },
|
||||
{ value: '.dev', label: '.dev', hot: true, price: 50, category: 'dev' },
|
||||
{ value: '.app', label: '.app', hot: true, price: 50, category: 'app' },
|
||||
{ value: '.fun', label: '.fun', hot: false, price: 45, category: 'fun' },
|
||||
{ value: '.tech', label: '.tech', hot: false, price: 45, category: 'tech' },
|
||||
{ value: '.art', label: '.art', hot: true, price: 45, category: 'art' },
|
||||
{ value: '.group', label: '.group', hot: false, price: 45, category: 'group' },
|
||||
{ value: '.net.cn', label: '.net.cn', hot: false, price: 45, category: 'cn' },
|
||||
{ value: '.work', label: '.work', hot: false, price: 45, category: 'work' },
|
||||
{ value: '.asia', label: '.asia', hot: false, price: 45, category: 'asia' },
|
||||
{ value: '.hk', label: '.hk', hot: true, price: 45, category: 'hk' },
|
||||
{ value: '.cc', label: '.cc', hot: false, price: 45, category: 'cc' },
|
||||
{ value: '.icu', label: '.icu', hot: true, price: 45, category: 'icu' },
|
||||
{ value: '.online', label: '.online', hot: false, price: 45, category: 'online' },
|
||||
{ value: '.xin', label: '.xin', hot: false, price: 45, category: 'xin' },
|
||||
{ value: '.club', label: '.club', hot: true, price: 45, category: 'club' },
|
||||
{ value: '.info', label: '.info', hot: false, price: 45, category: 'info' },
|
||||
{ value: '.ink', label: '.ink', hot: false, price: 45, category: 'ink' },
|
||||
{ value: '.love', label: '.love', hot: false, price: 45, category: 'love' },
|
||||
{ value: '.store', label: '.store', hot: false, price: 45, category: 'store' },
|
||||
{ value: '.中国', label: '.中国', hot: true, price: 45, category: 'chinese' },
|
||||
{ value: '.网络', label: '.网络', hot: false, price: 45, category: 'chinese' },
|
||||
{ value: '.公司', label: '.公司', hot: false, price: 45, category: 'chinese' },
|
||||
{ value: '.org.cn', label: '.org.cn', hot: false, price: 45, category: 'cn' },
|
||||
{ value: '.gov.cn', label: '.gov.cn', hot: false, price: 45, category: 'cn' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fullDomain() {
|
||||
if (!this.domainName) return '';
|
||||
return this.domainName + this.selectedSuffix;
|
||||
},
|
||||
filteredSuffixes() {
|
||||
let suffixes = this.allSuffixes;
|
||||
|
||||
// 根据分类过滤
|
||||
if (this.selectedCategory === 'popular') {
|
||||
suffixes = suffixes.filter(s => s.hot);
|
||||
} else if (this.selectedCategory === 'gtd') {
|
||||
suffixes = suffixes.filter(s => ['.com', '.net', '.org', '.info'].includes(s.value));
|
||||
} else if (this.selectedCategory === 'ntd') {
|
||||
suffixes = suffixes.filter(s => ['.xyz', '.top', '.vip', '.site', '.shop', '.club', '.icu', '.online', '.love', '.store'].includes(s.value));
|
||||
} else if (this.selectedCategory === 'country') {
|
||||
suffixes = suffixes.filter(s => ['.cn', '.hk', '.asia', '.cc'].includes(s.value));
|
||||
} else if (this.selectedCategory === 'cn') {
|
||||
suffixes = suffixes.filter(s => s.value.includes('.cn'));
|
||||
} else if (this.selectedCategory === 'chinese') {
|
||||
suffixes = suffixes.filter(s => s.category === 'chinese');
|
||||
}
|
||||
|
||||
// 根据搜索关键词过滤
|
||||
if (this.suffixSearch) {
|
||||
const searchLower = this.suffixSearch.toLowerCase();
|
||||
suffixes = suffixes.filter(s => s.label.toLowerCase().includes(searchLower));
|
||||
}
|
||||
|
||||
// 根据排序方式排序
|
||||
if (this.suffixSort === 'popular') {
|
||||
suffixes.sort((a, b) => (b.hot ? 1 : 0) - (a.hot ? 1 : 0));
|
||||
} else if (this.suffixSort === 'alphabetical') {
|
||||
suffixes.sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
return suffixes;
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
// 查询域名可用性
|
||||
checkDomainAvailability() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.check_domain',
|
||||
validate() {
|
||||
if (!this.domainName) {
|
||||
throw new DashboardError('请输入域名');
|
||||
}
|
||||
},
|
||||
onSuccess(response) {
|
||||
if (response.status === "Error") {
|
||||
this.error = response.message || '域名查询失败';
|
||||
this.domainCheckResult = { available: false };
|
||||
return;
|
||||
}
|
||||
|
||||
this.domainCheckResult = response;
|
||||
this.error = null;
|
||||
|
||||
// 如果域名可用,获取价格
|
||||
if (response.available) {
|
||||
this.getDomainPrice();
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
this.error = error.message || '域名查询失败';
|
||||
this.domainCheckResult = { available: false };
|
||||
}
|
||||
};
|
||||
},
|
||||
// 获取域名价格
|
||||
getDomainPrice() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.get_west_domain_price',
|
||||
onSuccess(response) {
|
||||
if (response.status === "Error") {
|
||||
this.domainPrice = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析价格信息
|
||||
if (response.data && response.data.price) {
|
||||
this.domainPrice = response.data.price;
|
||||
} else {
|
||||
this.domainPrice = 50; // 默认价格
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
this.domainPrice = 50; // 默认价格
|
||||
}
|
||||
};
|
||||
},
|
||||
// 创建域名注册订单
|
||||
createDomainOrder() {
|
||||
return {
|
||||
url: 'jcloud.api.domain_west.create_domain_order',
|
||||
validate() {
|
||||
if (!this.domainCheckResult || !this.domainCheckResult.available) {
|
||||
throw new DashboardError('请先查询域名可用性');
|
||||
}
|
||||
if (!this.selectedPaymentMethod) {
|
||||
throw new DashboardError('请选择支付方式');
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (!data.success) {
|
||||
this.error = data.message || '创建域名订单失败';
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示订单支付界面
|
||||
this.order = data.order;
|
||||
this.domain = data.domain;
|
||||
this.showPaymentProcessing = true;
|
||||
|
||||
// 立即处理支付
|
||||
this.processPayment();
|
||||
},
|
||||
onError(error) {
|
||||
this.error = error.message || '创建域名订单失败';
|
||||
}
|
||||
};
|
||||
},
|
||||
// 处理余额支付
|
||||
processBalancePayment() {
|
||||
return {
|
||||
url: 'jcloud.api.billing.process_balance_payment_for_domain_order',
|
||||
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.isProcessingPayment = false;
|
||||
this.paymentSuccess = true;
|
||||
|
||||
// 支付成功后跳转到域名列表
|
||||
setTimeout(() => {
|
||||
this.$router.push('/domains');
|
||||
}, 30000);
|
||||
},
|
||||
onError(error) {
|
||||
this.error = error.message || '余额支付处理失败';
|
||||
this.isProcessingPayment = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
// 处理支付宝支付
|
||||
processAlipayPayment() {
|
||||
return {
|
||||
url: 'jcloud.api.billing.process_alipay_order',
|
||||
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',
|
||||
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();
|
||||
|
||||
this.paymentSuccess = true;
|
||||
|
||||
// 支付成功后跳转到域名列表
|
||||
setTimeout(() => {
|
||||
this.$router.push('/domains');
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopPaymentCheck();
|
||||
},
|
||||
methods: {
|
||||
async checkDomain() {
|
||||
if (!this.domainName) {
|
||||
this.error = '请输入域名';
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.isChecking = true;
|
||||
|
||||
await this.$resources.checkDomainAvailability.submit({
|
||||
domain: this.domainName,
|
||||
suffix: this.selectedSuffix
|
||||
});
|
||||
|
||||
this.isChecking = false;
|
||||
},
|
||||
async getDomainPrice() {
|
||||
await this.$resources.getDomainPrice.submit({
|
||||
domain: this.fullDomain,
|
||||
year: 1
|
||||
});
|
||||
},
|
||||
async registerDomain() {
|
||||
if (!this.domainCheckResult || !this.domainCheckResult.available) {
|
||||
this.error = '请先查询域名可用性';
|
||||
return;
|
||||
}
|
||||
if (!this.selectedPaymentMethod) {
|
||||
this.error = '请选择支付方式';
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.isLoading = true;
|
||||
|
||||
await this.$resources.createDomainOrder.submit({
|
||||
domain: this.fullDomain,
|
||||
period: this.period,
|
||||
payment_method: this.selectedPaymentMethod
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
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
|
||||
});
|
||||
}
|
||||
},
|
||||
goToDomainList() {
|
||||
this.$router.push('/domains');
|
||||
},
|
||||
getTotalAmount() {
|
||||
const yearlyPrice = this.domainPrice || 0;
|
||||
return (yearlyPrice * this.period).toFixed(2);
|
||||
}
|
||||
},
|
||||
};
|
||||
</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;
|
||||
}
|
||||
|
||||
/* 域名后缀网格样式 */
|
||||
.domain-suffix-grid {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.domain-suffix-item {
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.domain-suffix-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.domain-suffix-item.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.domain-suffix-item.selected .hot-tag {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 分类标签样式 */
|
||||
.category-tabs {
|
||||
margin-bottom: 16px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.category-tab.active {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border-color: #3b82f6;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.category-tab:not(.active) {
|
||||
background: white;
|
||||
color: #6b7280;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.category-tab:not(.active):hover {
|
||||
background: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
/* 搜索框样式 */
|
||||
.search-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding-left: 36px;
|
||||
padding-right: 12px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #9ca3af;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* 热门标签样式 */
|
||||
.hot-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 添加渐入动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 640px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.domain-suffix-item {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.domain-suffix-item .text-sm {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 14px;
|
||||
padding: 8px 12px 8px 32px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 768px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1280px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1281px) and (max-width: 1536px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1537px) {
|
||||
.domain-suffix-grid {
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -131,6 +131,27 @@ let router = createRouter({
|
||||
return { objectType: 'Jsite Server', ...route.params };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Jsite Domain List',
|
||||
path: '/domains',
|
||||
component: () => import('./pages/ListPage.vue'),
|
||||
props: route => {
|
||||
return { objectType: 'Jsite Domain', ...route.params };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'New Jsite Domain',
|
||||
path: '/domains/new',
|
||||
component: () => import('./pages/NewJsiteDomain.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Jsite Domain Detail',
|
||||
path: '/domains/:name',
|
||||
component: () => import('./pages/DetailPage.vue'),
|
||||
props: route => {
|
||||
return { objectType: 'Jsite Domain', ...route.params };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
path: '/billing',
|
||||
|
||||
@ -1098,6 +1098,12 @@ def handle_order_payment_complete(order_id):
|
||||
elif order.order_type == "新建服务器":
|
||||
# 异步创建服务器
|
||||
jingrow.enqueue('jcloud.api.aliyun_server_light.create_aliyun_server', order_name=order.name)
|
||||
elif order.order_type == "域名注册":
|
||||
# 异步注册域名
|
||||
jingrow.enqueue('jcloud.api.domain_west.register_domain_from_order', order_name=order.name)
|
||||
elif order.order_type == "域名续费":
|
||||
# 异步续费域名
|
||||
jingrow.enqueue('jcloud.api.domain_west.renew_domain_from_order', order_name=order.name)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
@ -1948,3 +1954,80 @@ def get_balance_transactions(page=1, page_size=20, search=None):
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
@jingrow.whitelist()
|
||||
def process_balance_payment_for_domain_order(order_id):
|
||||
"""处理域名订单的余额支付"""
|
||||
try:
|
||||
# 获取当前用户团队
|
||||
team = get_current_team(True)
|
||||
|
||||
# 获取订单信息
|
||||
order = jingrow.get_pg("Order", {"order_id": order_id})
|
||||
if not order:
|
||||
jingrow.throw(f"找不到订单: {order_id}")
|
||||
|
||||
# 验证订单是否属于当前团队
|
||||
if order.team != team.name:
|
||||
jingrow.throw("您没有权限支付此订单")
|
||||
|
||||
# 检查订单状态
|
||||
if order.status != "待支付":
|
||||
return {
|
||||
"success": False,
|
||||
"message": "该订单已支付或已取消"
|
||||
}
|
||||
|
||||
# 使用 Team 类的 get_balance 方法获取余额
|
||||
balance = team.get_balance()
|
||||
|
||||
# 检查余额是否足够
|
||||
if balance < order.total_amount:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "余额不足"
|
||||
}
|
||||
|
||||
# 创建余额交易记录(扣款)
|
||||
balance_transaction = jingrow.get_pg({
|
||||
"pagetype": "Balance Transaction",
|
||||
"team": team.name,
|
||||
"type": "Adjustment",
|
||||
"source": "Prepaid Credits",
|
||||
"amount": -1 * float(order.total_amount), # 使用负数表示扣减
|
||||
"description": f"{order.order_type}-{order.title}",
|
||||
"paid_via_local_pg": 1
|
||||
})
|
||||
balance_transaction.flags.ignore_permissions = True
|
||||
balance_transaction.insert()
|
||||
balance_transaction.submit()
|
||||
|
||||
# 更新订单状态
|
||||
order.status = "已支付"
|
||||
order.payment_method = "余额支付"
|
||||
order.save(ignore_permissions=True)
|
||||
jingrow.db.commit()
|
||||
|
||||
# 支付成功,订单状态已更新
|
||||
|
||||
# 调用统一的订单支付完成处理函数
|
||||
handle_order_payment_complete(order_id)
|
||||
|
||||
return {
|
||||
"status": "Success",
|
||||
"message": "支付成功",
|
||||
"order": order.as_dict()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("支付错误", f"域名订单余额支付失败: {str(e)}")
|
||||
return {
|
||||
"status": "Error",
|
||||
"message": f"余额支付失败: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -78,6 +78,7 @@ ALLOWED_PAGETYPES = [
|
||||
"Jcloud Settings",
|
||||
"Mpesa Payment Record",
|
||||
"Jsite Server",
|
||||
"Jsite Domain",
|
||||
]
|
||||
|
||||
ALLOWED_PAGETYPES_FOR_SUPPORT = [
|
||||
|
||||
@ -408,7 +408,33 @@ def check_domain(domain: str, suffix: str = '.com'):
|
||||
if not domain:
|
||||
return {"status": "error", "message": "缺少域名参数"}
|
||||
|
||||
return client.query_domain(domain, suffix)
|
||||
response = client.query_domain(domain, suffix)
|
||||
|
||||
# 添加调试日志
|
||||
jingrow.log_error("域名查询调试", f"domain={domain}, suffix={suffix}, response={response}")
|
||||
|
||||
if response.get("status") == "error":
|
||||
return response
|
||||
|
||||
try:
|
||||
# 直接检查响应格式
|
||||
if response.get("result") != 200:
|
||||
return {"status": "error", "message": "API查询失败"}
|
||||
|
||||
full_domain = domain + suffix
|
||||
for item in response.get("data", []):
|
||||
if item.get("name") == full_domain:
|
||||
return {
|
||||
"available": item.get("avail", 0) == 1,
|
||||
"domain": full_domain,
|
||||
"message": "域名可用" if item.get("avail", 0) == 1 else "域名已被注册"
|
||||
}
|
||||
|
||||
return {"status": "error", "message": f"未找到域名 {full_domain} 的查询结果"}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名查询响应解析失败", error=str(e))
|
||||
return {"status": "error", "message": "域名查询响应解析失败"}
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
@ -421,7 +447,28 @@ def get_west_domain_price(domain: str, year: int = 1):
|
||||
if not domain:
|
||||
return {"status": "error", "message": "缺少域名参数"}
|
||||
|
||||
return client.get_domain_price(domain, year)
|
||||
response = client.get_domain_price(domain, year)
|
||||
|
||||
if response.get("status") == "error":
|
||||
return response
|
||||
|
||||
try:
|
||||
# 直接检查响应格式
|
||||
if response.get("result") != 200:
|
||||
return {"status": "error", "message": "API查询失败"}
|
||||
|
||||
data = response.get("data", {})
|
||||
return {
|
||||
"data": {
|
||||
"price": data.get("buyprice", 0),
|
||||
"domain": domain,
|
||||
"year": year
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名价格查询响应解析失败", error=str(e))
|
||||
return {"status": "error", "message": "域名价格查询响应解析失败"}
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
@ -628,41 +675,294 @@ def west_domain_get_template_detail(**data):
|
||||
return client.get_template_detail(template_id)
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def call_west_domain_api(api_name: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
调用西部数码域名API的通用函数
|
||||
@jingrow.whitelist()
|
||||
def create_domain_order(domain, period=1, payment_method='balance'):
|
||||
"""创建域名注册订单"""
|
||||
try:
|
||||
# 获取当前用户团队
|
||||
team = get_current_team(True)
|
||||
|
||||
Args:
|
||||
api_name: API名称
|
||||
**kwargs: API参数
|
||||
# 验证域名格式
|
||||
if not domain or '.' not in domain:
|
||||
return {"success": False, "message": "域名格式不正确"}
|
||||
|
||||
Returns:
|
||||
API响应结果
|
||||
"""
|
||||
api_functions = {
|
||||
'check_balance': check_west_balance,
|
||||
'query': check_domain,
|
||||
'get_price': get_west_domain_price,
|
||||
'register': west_domain_register,
|
||||
'renew': west_domain_renew,
|
||||
'get_list': west_domain_get_list,
|
||||
'get_info': west_domain_get_info,
|
||||
'get_dns': west_domain_get_dns,
|
||||
'modify_dns': west_domain_modify_dns,
|
||||
'add_dns_record': west_domain_add_dns_record,
|
||||
'delete_dns_record': west_domain_delete_dns_record,
|
||||
'transfer': west_domain_transfer,
|
||||
'lock': west_domain_lock,
|
||||
'get_templates': west_domain_get_templates,
|
||||
'get_template_detail': west_domain_get_template_detail,
|
||||
# 查询域名价格
|
||||
client = get_west_client()
|
||||
if not client:
|
||||
return {"success": False, "message": "API客户端初始化失败"}
|
||||
|
||||
price_result = client.get_domain_price(domain, 1)
|
||||
if price_result.get("status") == "error":
|
||||
return {"success": False, "message": "获取域名价格失败"}
|
||||
|
||||
# 计算总价格
|
||||
yearly_price = price_result.get("data", {}).get("price", 50) # 默认50元/年
|
||||
total_amount = yearly_price * period
|
||||
|
||||
# 生成订单号
|
||||
order_id = f"DOMAIN_{jingrow.utils.random_string(6)}"
|
||||
|
||||
# 创建订单记录
|
||||
order = jingrow.get_pg({
|
||||
"pagetype": "Order",
|
||||
"order_id": order_id,
|
||||
"order_type": "域名注册",
|
||||
"team": team.name,
|
||||
"status": "待支付",
|
||||
"total_amount": total_amount,
|
||||
"title": domain,
|
||||
"description": f"{period}年"
|
||||
})
|
||||
order.insert(ignore_permissions=True)
|
||||
|
||||
# 创建域名记录
|
||||
domain_doc = jingrow.get_pg({
|
||||
"pagetype": "Jsite Domain",
|
||||
"domain": domain,
|
||||
"team": team.name,
|
||||
"order_id": order_id,
|
||||
"status": "Pending",
|
||||
"price": yearly_price,
|
||||
"period": period,
|
||||
"domain_registrar": "西部数码",
|
||||
"auto_renew": False,
|
||||
"whois_protection": True
|
||||
})
|
||||
domain_doc.insert(ignore_permissions=True)
|
||||
|
||||
jingrow.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "订单创建成功",
|
||||
"order": order.as_dict(),
|
||||
"domain": domain_doc.as_dict()
|
||||
}
|
||||
|
||||
if api_name not in api_functions:
|
||||
return {"status": "error", "message": f"未知的API: {api_name}"}
|
||||
|
||||
try:
|
||||
return api_functions[api_name](**kwargs)
|
||||
except Exception as e:
|
||||
jingrow.log_error(f"西部数码域名API调用失败: {api_name}", error=str(e), params=kwargs)
|
||||
return {"status": "error", "message": f"API调用失败: {str(e)}"}
|
||||
jingrow.log_error("域名订单", f"创建域名订单失败: {str(e)}")
|
||||
return {"success": False, "message": f"创建订单失败: {str(e)}"}
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def create_domain_renew_order(**kwargs):
|
||||
"""创建域名续费订单"""
|
||||
try:
|
||||
domain = kwargs.get('domain')
|
||||
renewal_years = kwargs.get('renewal_years', 1)
|
||||
|
||||
if not domain:
|
||||
jingrow.throw("缺少域名信息")
|
||||
|
||||
# 验证输入
|
||||
domain_pg = jingrow.get_pg("Jsite Domain", domain)
|
||||
if not domain_pg:
|
||||
jingrow.throw("域名不存在")
|
||||
|
||||
team = domain_pg.team
|
||||
|
||||
# 验证当前用户权限
|
||||
current_team = get_current_team(True)
|
||||
if current_team.name != team:
|
||||
jingrow.throw("您没有权限为此域名创建续费订单")
|
||||
|
||||
# 计算续费金额
|
||||
renewal_years = int(renewal_years)
|
||||
yearly_price = domain_pg.price or 0
|
||||
total_amount = yearly_price * renewal_years
|
||||
|
||||
# 生成唯一订单号
|
||||
order_id = f"DOMAIN_RENEW_{jingrow.utils.random_string(6)}"
|
||||
|
||||
# 创建订单记录
|
||||
order = jingrow.get_pg({
|
||||
"pagetype": "Order",
|
||||
"order_id": order_id,
|
||||
"order_type": "域名续费",
|
||||
"team": team,
|
||||
"status": "待支付",
|
||||
"total_amount": total_amount,
|
||||
"title": domain_pg.domain,
|
||||
"description": str(renewal_years) # 存储续费年数
|
||||
})
|
||||
|
||||
order.insert(ignore_permissions=True)
|
||||
jingrow.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"order": order.as_dict()
|
||||
}
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名续费订单错误", f"创建域名续费订单失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"创建续费订单失败: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def register_domain_from_order(order_name):
|
||||
"""支付成功后异步注册域名"""
|
||||
try:
|
||||
order = jingrow.get_pg("Order", order_name)
|
||||
if not order:
|
||||
raise Exception("订单不存在")
|
||||
|
||||
# 查找对应的域名记录(通过订单ID)
|
||||
domain = jingrow.get_pg("Jsite Domain", {"order_id": order.order_id})
|
||||
if not domain:
|
||||
raise Exception("找不到对应的域名记录")
|
||||
|
||||
# 从域名记录中获取配置信息
|
||||
domain_name = domain.domain
|
||||
period = domain.period or 1
|
||||
|
||||
# 调用西部数码API注册域名
|
||||
result = call_west_domain_api("register", domain=domain_name, regyear=period)
|
||||
|
||||
# 打印result到后台日志
|
||||
jingrow.log_error("西部数码域名注册结果", f"订单 {order_name} 的注册结果: {result}")
|
||||
|
||||
if not result or not result.get('success'):
|
||||
raise Exception(f"域名注册失败: {result.get('message', '未知错误')}")
|
||||
|
||||
# 更新域名记录状态
|
||||
domain.status = "Active"
|
||||
domain.registration_date = jingrow.utils.nowdate()
|
||||
domain.end_date = jingrow.utils.add_months(jingrow.utils.nowdate(), period * 12)
|
||||
domain.save(ignore_permissions=True)
|
||||
|
||||
# 更新订单状态
|
||||
order.status = "交易成功"
|
||||
order.save(ignore_permissions=True)
|
||||
|
||||
jingrow.db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名注册失败", f"订单 {order_name}: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
||||
def renew_domain_from_order(order_name):
|
||||
"""支付成功后异步续费域名"""
|
||||
try:
|
||||
order = jingrow.get_pg("Order", order_name)
|
||||
if not order:
|
||||
raise Exception("订单不存在")
|
||||
|
||||
# 从订单中获取信息
|
||||
domain_name = order.title # 域名
|
||||
renewal_years = int(order.description) # 续费年数
|
||||
|
||||
# 查找域名记录
|
||||
domain = jingrow.get_pg("Jsite Domain", {"domain": domain_name})
|
||||
if not domain:
|
||||
raise Exception("找不到对应的域名记录")
|
||||
|
||||
# 调用西部数码API续费域名
|
||||
result = call_west_domain_api("renew", domain=domain_name, regyear=renewal_years)
|
||||
|
||||
# 打印result到后台日志
|
||||
jingrow.log_error("西部数码域名续费结果", f"订单 {order_name} 的续费结果: {result}")
|
||||
|
||||
if not result or not result.get('success'):
|
||||
raise Exception(f"域名续费失败: {result.get('message', '未知错误')}")
|
||||
|
||||
# 更新域名到期时间
|
||||
domain.end_date = jingrow.utils.add_months(domain.end_date or jingrow.utils.nowdate(), renewal_years * 12)
|
||||
domain.save(ignore_permissions=True)
|
||||
|
||||
# 更新订单状态
|
||||
order.status = "交易成功"
|
||||
order.save(ignore_permissions=True)
|
||||
|
||||
jingrow.db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名续费失败", f"订单 {order_name}: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def toggle_domain_auto_renew(pagetype, name, auto_renew):
|
||||
"""切换域名自动续费状态"""
|
||||
try:
|
||||
# 获取当前用户团队
|
||||
team = get_current_team(True)
|
||||
|
||||
# 获取域名记录
|
||||
domain = jingrow.get_pg(pagetype, name)
|
||||
if not domain:
|
||||
return {"success": False, "message": "找不到域名记录"}
|
||||
|
||||
# 验证权限
|
||||
if domain.team != team.name:
|
||||
return {"success": False, "message": "您没有权限操作此域名"}
|
||||
|
||||
# 更新自动续费状态
|
||||
domain.auto_renew = bool(auto_renew)
|
||||
domain.save(ignore_permissions=True)
|
||||
|
||||
return {"success": True, "message": "自动续费状态更新成功"}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名管理", f"切换自动续费状态失败: {str(e)}")
|
||||
return {"success": False, "message": f"操作失败: {str(e)}"}
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def toggle_domain_whois_protection(pagetype, name, whois_protection):
|
||||
"""切换域名隐私保护状态"""
|
||||
try:
|
||||
# 获取当前用户团队
|
||||
team = get_current_team(True)
|
||||
|
||||
# 获取域名记录
|
||||
domain = jingrow.get_pg(pagetype, name)
|
||||
if not domain:
|
||||
return {"success": False, "message": "找不到域名记录"}
|
||||
|
||||
# 验证权限
|
||||
if domain.team != team.name:
|
||||
return {"success": False, "message": "您没有权限操作此域名"}
|
||||
|
||||
# 更新隐私保护状态
|
||||
domain.whois_protection = bool(whois_protection)
|
||||
domain.save(ignore_permissions=True)
|
||||
|
||||
return {"success": True, "message": "隐私保护状态更新成功"}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名管理", f"切换隐私保护状态失败: {str(e)}")
|
||||
return {"success": False, "message": f"操作失败: {str(e)}"}
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def delete_domain(pagetype, name):
|
||||
"""删除域名记录"""
|
||||
try:
|
||||
# 获取当前用户团队
|
||||
team = get_current_team(True)
|
||||
|
||||
# 获取域名记录
|
||||
domain = jingrow.get_pg(pagetype, name)
|
||||
if not domain:
|
||||
return {"success": False, "message": "找不到域名记录"}
|
||||
|
||||
# 验证权限
|
||||
if domain.team != team.name:
|
||||
return {"success": False, "message": "您没有权限操作此域名"}
|
||||
|
||||
# 删除域名记录
|
||||
domain.delete(ignore_permissions=True)
|
||||
|
||||
return {"success": True, "message": "域名记录删除成功"}
|
||||
|
||||
except Exception as e:
|
||||
jingrow.log_error("域名管理", f"删除域名记录失败: {str(e)}")
|
||||
return {"success": False, "message": f"删除失败: {str(e)}"}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2025, Jingrow and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import jingrow
|
||||
import jingrow
|
||||
from jingrow.model.document import Document
|
||||
|
||||
|
||||
@ -38,4 +38,36 @@ class JsiteDomain(Document):
|
||||
team: DF.Link | None
|
||||
whois_protection: DF.Check
|
||||
# end: auto-generated types
|
||||
pass
|
||||
|
||||
dashboard_fields = (
|
||||
"domain",
|
||||
"status",
|
||||
"domain_owner",
|
||||
"domain_registrar",
|
||||
"registration_date",
|
||||
"end_date",
|
||||
"price",
|
||||
"period",
|
||||
"auto_renew",
|
||||
"team",
|
||||
"order_id",
|
||||
"description",
|
||||
"whois_protection",
|
||||
"admin_password",
|
||||
"group",
|
||||
"dns_host1",
|
||||
"dns_host2",
|
||||
"dns_host3",
|
||||
"dns_host4",
|
||||
"dns_host5",
|
||||
"dns_host6"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_list_query(query):
|
||||
JsiteDomain = jingrow.qb.PageType("Jsite Domain")
|
||||
query = query.where(JsiteDomain.team == jingrow.local.team().name)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def get_pg(self, pg):
|
||||
return pg
|
||||
|
||||
@ -77,15 +77,15 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Order Type",
|
||||
"options": "\n余额充值\n新建网站\n网站续费\n域名续费\n新建服务器\n服务器续费\n服务器升级",
|
||||
"options": "\n余额充值\n新建网站\n网站续费\n域名注册\n域名续费\n新建服务器\n服务器续费\n服务器升级",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-30 23:11:19.817353",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-01 00:44:25.874899",
|
||||
"modified_by": "support@jingrow.com",
|
||||
"module": "Jcloud",
|
||||
"name": "Order",
|
||||
"owner": "Administrator",
|
||||
|
||||
@ -16,7 +16,7 @@ class Order(Document):
|
||||
|
||||
description: DF.Data | None
|
||||
order_id: DF.Data | None
|
||||
order_type: DF.Literal["", "\u4f59\u989d\u5145\u503c", "\u65b0\u5efa\u7f51\u7ad9", "\u7f51\u7ad9\u7eed\u8d39", "\u57df\u540d\u7eed\u8d39", "\u65b0\u5efa\u670d\u52a1\u5668", "\u670d\u52a1\u5668\u7eed\u8d39", "\u670d\u52a1\u5668\u5347\u7ea7"]
|
||||
order_type: DF.Literal["", "\u4f59\u989d\u5145\u503c", "\u65b0\u5efa\u7f51\u7ad9", "\u7f51\u7ad9\u7eed\u8d39", "\u57df\u540d\u6ce8\u518c", "\u57df\u540d\u7eed\u8d39", "\u65b0\u5efa\u670d\u52a1\u5668", "\u670d\u52a1\u5668\u7eed\u8d39", "\u670d\u52a1\u5668\u5347\u7ea7"]
|
||||
payment_method: DF.Literal["", "\u652f\u4ed8\u5b9d", "\u5fae\u4fe1\u652f\u4ed8", "\u4f59\u989d\u652f\u4ed8", "\u94f6\u884c\u8f6c\u8d26", "\u5176\u4ed6"]
|
||||
status: DF.Literal["\u5f85\u652f\u4ed8", "\u5df2\u652f\u4ed8", "\u4ea4\u6613\u6210\u529f", "\u5df2\u53d6\u6d88", "\u5df2\u9000\u6b3e"]
|
||||
team: DF.Link | None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user