dashboard左侧增加域名菜单及实现相关功能

This commit is contained in:
jingrow 2025-08-01 02:12:18 +08:00
parent f6b2e9dd31
commit 7046ab9850
14 changed files with 2545 additions and 44 deletions

View 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>

View 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>

View 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>

View File

@ -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),

View 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 [];
}
}
};

View File

@ -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
};

View 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>

View File

@ -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',

View File

@ -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)}"
}

View File

@ -78,6 +78,7 @@ ALLOWED_PAGETYPES = [
"Jcloud Settings",
"Mpesa Payment Record",
"Jsite Server",
"Jsite Domain",
]
ALLOWED_PAGETYPES_FOR_SUPPORT = [

View File

@ -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的通用函数
Args:
api_name: API名称
**kwargs: API参数
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,
}
if api_name not in api_functions:
return {"status": "error", "message": f"未知的API: {api_name}"}
@jingrow.whitelist()
def create_domain_order(domain, period=1, payment_method='balance'):
"""创建域名注册订单"""
try:
return api_functions[api_name](**kwargs)
# 获取当前用户团队
team = get_current_team(True)
# 验证域名格式
if not domain or '.' not in domain:
return {"success": False, "message": "域名格式不正确"}
# 查询域名价格
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()
}
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)}"}

View File

@ -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

View File

@ -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",

View File

@ -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