增加域名解析标签页

This commit is contained in:
jingrow 2025-08-03 19:55:03 +08:00
parent e723ada8fb
commit c42115a0ad
5 changed files with 887 additions and 3 deletions

View File

@ -0,0 +1,246 @@
<template>
<Dialog>
<DialogContent class="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>添加DNS记录</DialogTitle>
<DialogDescription>
为域名 {{ domainDoc?.domain }} 添加新的DNS解析记录
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitForm" class="space-y-4">
<!-- 记录类型 -->
<div class="space-y-2">
<Label for="record-type">记录类型</Label>
<Select v-model="form.record_type" required>
<SelectTrigger>
<SelectValue placeholder="选择记录类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="A">A - IPv4地址</SelectItem>
<SelectItem value="AAAA">AAAA - IPv6地址</SelectItem>
<SelectItem value="CNAME">CNAME - 别名</SelectItem>
<SelectItem value="MX">MX - 邮件交换</SelectItem>
<SelectItem value="TXT">TXT - 文本记录</SelectItem>
<SelectItem value="NS">NS - 域名服务器</SelectItem>
</SelectContent>
</Select>
</div>
<!-- 主机记录 -->
<div class="space-y-2">
<Label for="host">主机记录</Label>
<Input
id="host"
v-model="form.host"
placeholder="例如: www 或 @ (留空表示根域名)"
required
/>
<p class="text-xs text-gray-500">
@ 表示根域名www 表示 www.yourdomain.com
</p>
</div>
<!-- 记录值 -->
<div class="space-y-2">
<Label for="value">记录值</Label>
<Input
id="value"
v-model="form.value"
:placeholder="getValuePlaceholder()"
required
/>
<p class="text-xs text-gray-500">
{{ getValueDescription() }}
</p>
</div>
<!-- TTL -->
<div class="space-y-2">
<Label for="ttl">TTL ()</Label>
<Input
id="ttl"
v-model.number="form.ttl"
type="number"
min="60"
max="86400"
placeholder="600"
/>
<p class="text-xs text-gray-500">
建议值600 (10分钟) 86400 (24小时)
</p>
</div>
<!-- MX优先级 (仅MX记录显示) -->
<div v-if="form.record_type === 'MX'" class="space-y-2">
<Label for="mx-priority">MX优先级</Label>
<Input
id="mx-priority"
v-model.number="form.mx_priority"
type="number"
min="0"
max="65535"
placeholder="10"
/>
<p class="text-xs text-gray-500">
数值越小优先级越高建议值10
</p>
</div>
</form>
<DialogFooter>
<Button @click="$emit('close')" variant="outline">
取消
</Button>
<Button @click="submitForm" :loading="loading" variant="solid">
添加记录
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script>
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
createResource
} from 'jingrow-ui';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../utils/toast';
export default {
name: 'JsiteDomainAddDNSRecordDialog',
props: ['domain', 'domainDoc', 'onSuccess'],
emits: ['close'],
data() {
return {
loading: false,
form: {
record_type: 'A',
host: '',
value: '',
ttl: 600,
mx_priority: 10
}
};
},
methods: {
//
getValuePlaceholder() {
const placeholders = {
'A': '例如: 192.168.1.1',
'AAAA': '例如: 2001:db8::1',
'CNAME': '例如: example.com',
'MX': '例如: mail.example.com',
'TXT': '例如: 验证字符串',
'NS': '例如: ns1.example.com'
};
return placeholders[this.form.record_type] || '';
},
//
getValueDescription() {
const descriptions = {
'A': '输入IPv4地址192.168.1.1',
'AAAA': '输入IPv6地址2001:db8::1',
'CNAME': '输入目标域名example.com',
'MX': '输入邮件服务器域名mail.example.com',
'TXT': '输入文本内容,常用于域名验证',
'NS': '输入域名服务器地址ns1.example.com'
};
return descriptions[this.form.record_type] || '';
},
//
validateForm() {
if (!this.form.record_type) {
toast.error('请选择记录类型');
return false;
}
if (!this.form.value) {
toast.error('请输入记录值');
return false;
}
if (this.form.ttl < 60 || this.form.ttl > 86400) {
toast.error('TTL值必须在60-86400秒之间');
return false;
}
if (this.form.record_type === 'MX' && (this.form.mx_priority < 0 || this.form.mx_priority > 65535)) {
toast.error('MX优先级必须在0-65535之间');
return false;
}
return true;
},
//
async submitForm() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
//
const recordData = {
type: this.form.record_type,
host: this.form.host || '@',
value: this.form.value,
ttl: this.form.ttl
};
// MX
if (this.form.record_type === 'MX') {
recordData.level = this.form.mx_priority;
}
const request = createResource({
url: 'jcloud.api.domain_west.west_domain_add_dns_record',
params: {
domain: this.domainDoc.domain,
record_type: this.form.record_type,
host: this.form.host || '@',
value: this.form.value,
ttl: this.form.ttl,
level: this.form.mx_priority
},
onSuccess: (response) => {
this.loading = false;
if (response.status === 'success') {
toast.success('DNS记录添加成功');
this.$emit('close');
if (this.onSuccess) {
this.onSuccess();
}
} else {
toast.error(response.message || '添加DNS记录失败');
}
},
onError: (error) => {
this.loading = false;
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
this.loading = false;
toast.error('添加DNS记录失败');
console.error('添加DNS记录失败:', error);
}
}
}
};
</script>

View File

@ -0,0 +1,347 @@
<template>
<div class="space-y-6">
<!-- 标题和操作按钮 -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-medium text-gray-900">域名解析记录</h2>
<p class="mt-1 text-sm text-gray-500">管理域名的DNS解析记录</p>
</div>
<div class="flex gap-2">
<Button
@click="refreshRecords"
:loading="loading"
variant="outline"
size="sm"
>
<RefreshCwIcon class="h-4 w-4 mr-1" />
刷新
</Button>
<Button
@click="showAddRecordDialog"
variant="solid"
size="sm"
>
<PlusIcon class="h-4 w-4 mr-1" />
添加记录
</Button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="flex items-center space-x-2">
<Loader2Icon class="h-5 w-5 animate-spin text-gray-500" />
<span class="text-gray-500">正在加载DNS记录...</span>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="rounded-md border border-red-200 bg-red-50 p-4">
<div class="flex items-center">
<AlertCircleIcon class="h-5 w-5 text-red-400 mr-2" />
<div>
<h3 class="text-sm font-medium text-red-800">加载失败</h3>
<p class="text-sm text-red-700 mt-1">{{ error }}</p>
</div>
</div>
</div>
<!-- DNS记录列表 -->
<div v-else-if="dnsRecords.length > 0" class="space-y-4">
<div class="overflow-hidden rounded-md border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
类型
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
主机记录
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
记录值
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
TTL
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="record in dnsRecords" :key="record.id" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<Badge :variant="getRecordTypeVariant(record.type)">
{{ record.type }}
</Badge>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
{{ record.item || record.host || '@' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
{{ record.value }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ record.ttl || '600' }}
<span v-if="record.type === 'MX' && record.level" class="ml-2 text-xs text-blue-600">
(优先级: {{ record.level }})
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="flex items-center space-x-2">
<Button
@click="editRecord(record)"
variant="ghost"
size="sm"
>
<EditIcon class="h-4 w-4" />
</Button>
<Button
@click="deleteRecord(record)"
variant="ghost"
size="sm"
class="text-red-600 hover:text-red-700"
>
<Trash2Icon class="h-4 w-4" />
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="pagination.total > pagination.limit" class="flex items-center justify-between">
<div class="text-sm text-gray-700">
显示第 {{ (pagination.pageno - 1) * pagination.limit + 1 }} -
{{ Math.min(pagination.pageno * pagination.limit, pagination.total) }}
{{ pagination.total }} 条记录
</div>
<div class="flex items-center space-x-2">
<Button
@click="changePage(pagination.pageno - 1)"
:disabled="pagination.pageno <= 1"
variant="outline"
size="sm"
>
上一页
</Button>
<span class="text-sm text-gray-700">
{{ pagination.pageno }} / {{ pagination.pagecount }}
</span>
<Button
@click="changePage(pagination.pageno + 1)"
:disabled="pagination.pageno >= pagination.pagecount"
variant="outline"
size="sm"
>
下一页
</Button>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="text-center py-12">
<GlobeIcon class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无DNS记录</h3>
<p class="mt-1 text-sm text-gray-500">开始添加您的第一个DNS解析记录</p>
<div class="mt-6">
<Button @click="showAddRecordDialog" variant="solid">
<PlusIcon class="h-4 w-4 mr-1" />
添加记录
</Button>
</div>
</div>
</div>
</template>
<script>
import { getCachedDocumentResource, Badge, 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 RefreshCwIcon from '~icons/lucide/refresh-cw';
import PlusIcon from '~icons/lucide/plus';
import Loader2Icon from '~icons/lucide/loader-2';
import AlertCircleIcon from '~icons/lucide/alert-circle';
import EditIcon from '~icons/lucide/edit';
import Trash2Icon from '~icons/lucide/trash-2';
import GlobeIcon from '~icons/lucide/globe';
export default {
name: 'JsiteDomainDNSRecords',
props: ['domain'],
data() {
return {
loading: false,
error: null,
dnsRecords: [],
pagination: {
pageno: 1,
limit: 20,
total: 0,
pagecount: 0
}
};
},
methods: {
// DNS
async loadDNSRecords() {
if (!this.$domain.pg?.domain) {
this.error = '域名信息不存在';
return;
}
this.loading = true;
this.error = null;
try {
const request = createResource({
url: 'jcloud.api.domain_west.get_west_domain_dns_records',
params: {
domain: this.$domain.pg.domain,
limit: this.pagination.limit,
pageno: this.pagination.pageno
},
onSuccess: (response) => {
this.loading = false;
if (response.status === 'success' && response.data) {
this.dnsRecords = response.data.items || [];
this.pagination = {
pageno: response.data.pageno || 1,
limit: response.data.limit || 20,
total: response.data.total || 0,
pagecount: response.data.pagecount || 0
};
} else {
this.error = response.message || '获取DNS记录失败';
}
},
onError: (error) => {
this.loading = false;
this.error = getToastErrorMessage(error);
}
});
request.submit();
} catch (error) {
this.loading = false;
this.error = '获取DNS记录时发生错误';
console.error('加载DNS记录失败:', error);
}
},
//
refreshRecords() {
this.loadDNSRecords();
},
//
changePage(page) {
if (page >= 1 && page <= this.pagination.pagecount) {
this.pagination.pageno = page;
this.loadDNSRecords();
}
},
//
getRecordTypeVariant(type) {
const variantMap = {
'A': 'success',
'AAAA': 'info',
'CNAME': 'warning',
'MX': 'primary',
'TXT': 'secondary',
'NS': 'default'
};
return variantMap[type] || 'default';
},
//
showAddRecordDialog() {
const JsiteDomainAddDNSRecordDialog = defineAsyncComponent(() => import('./JsiteDomainAddDNSRecordDialog.vue'));
renderDialog(h(JsiteDomainAddDNSRecordDialog, {
domain: this.domain,
domainDoc: this.$domain.pg,
onSuccess: this.onRecordAdded
}));
},
//
editRecord(record) {
const JsiteDomainEditDNSRecordDialog = defineAsyncComponent(() => import('./JsiteDomainEditDNSRecordDialog.vue'));
renderDialog(h(JsiteDomainEditDNSRecordDialog, {
domain: this.domain,
domainDoc: this.$domain.pg,
record: record,
onSuccess: this.onRecordUpdated
}));
},
//
deleteRecord(record) {
confirmDialog({
title: '删除DNS记录',
message: `确定要删除这条DNS记录吗\n类型: ${record.type}\n主机记录: ${record.item || record.host || '@'}\n记录值: ${record.value}`,
primaryAction: {
label: '删除',
variant: 'danger',
onClick: ({ hide }) => {
this.performDeleteRecord(record, hide);
}
}
});
},
//
async performDeleteRecord(record, hide) {
try {
const request = createResource({
url: 'jcloud.api.domain_west.west_domain_delete_dns_record',
params: {
domain: this.$domain.pg.domain,
record_id: record.id
},
onSuccess: () => {
toast.success('DNS记录删除成功');
hide();
this.loadDNSRecords();
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('删除DNS记录失败');
console.error('删除DNS记录失败:', error);
}
},
//
onRecordAdded() {
toast.success('DNS记录添加成功');
this.loadDNSRecords();
},
//
onRecordUpdated() {
toast.success('DNS记录更新成功');
this.loadDNSRecords();
}
},
computed: {
$domain() {
return getCachedDocumentResource('Jsite Domain', this.domain);
}
},
mounted() {
this.loadDNSRecords();
}
};
</script>

View File

@ -0,0 +1,259 @@
<template>
<Dialog>
<DialogContent class="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>编辑DNS记录</DialogTitle>
<DialogDescription>
编辑域名 {{ domainDoc?.domain }} 的DNS解析记录
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitForm" class="space-y-4">
<!-- 记录类型 -->
<div class="space-y-2">
<Label for="record-type">记录类型</Label>
<Select v-model="form.record_type" required>
<SelectTrigger>
<SelectValue placeholder="选择记录类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="A">A - IPv4地址</SelectItem>
<SelectItem value="AAAA">AAAA - IPv6地址</SelectItem>
<SelectItem value="CNAME">CNAME - 别名</SelectItem>
<SelectItem value="MX">MX - 邮件交换</SelectItem>
<SelectItem value="TXT">TXT - 文本记录</SelectItem>
<SelectItem value="NS">NS - 域名服务器</SelectItem>
</SelectContent>
</Select>
</div>
<!-- 主机记录 -->
<div class="space-y-2">
<Label for="host">主机记录</Label>
<Input
id="host"
v-model="form.host"
placeholder="例如: www 或 @ (留空表示根域名)"
required
/>
<p class="text-xs text-gray-500">
@ 表示根域名www 表示 www.yourdomain.com
</p>
</div>
<!-- 记录值 -->
<div class="space-y-2">
<Label for="value">记录值</Label>
<Input
id="value"
v-model="form.value"
:placeholder="getValuePlaceholder()"
required
/>
<p class="text-xs text-gray-500">
{{ getValueDescription() }}
</p>
</div>
<!-- TTL -->
<div class="space-y-2">
<Label for="ttl">TTL ()</Label>
<Input
id="ttl"
v-model.number="form.ttl"
type="number"
min="60"
max="86400"
placeholder="600"
/>
<p class="text-xs text-gray-500">
建议值600 (10分钟) 86400 (24小时)
</p>
</div>
<!-- MX优先级 (仅MX记录显示) -->
<div v-if="form.record_type === 'MX'" class="space-y-2">
<Label for="mx-priority">MX优先级</Label>
<Input
id="mx-priority"
v-model.number="form.mx_priority"
type="number"
min="0"
max="65535"
placeholder="10"
/>
<p class="text-xs text-gray-500">
数值越小优先级越高建议值10
</p>
</div>
</form>
<DialogFooter>
<Button @click="$emit('close')" variant="outline">
取消
</Button>
<Button @click="submitForm" :loading="loading" variant="solid">
保存修改
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script>
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
createResource
} from 'jingrow-ui';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../utils/toast';
export default {
name: 'JsiteDomainEditDNSRecordDialog',
props: ['domain', 'domainDoc', 'record', 'onSuccess'],
emits: ['close'],
data() {
return {
loading: false,
form: {
record_type: 'A',
host: '',
value: '',
ttl: 600,
mx_priority: 10
}
};
},
methods: {
//
initForm() {
if (this.record) {
this.form = {
record_type: this.record.type || 'A',
host: this.record.item || this.record.host || '',
value: this.record.value || '',
ttl: this.record.ttl || 600,
mx_priority: this.record.level || this.record.mx_priority || 10
};
}
},
//
getValuePlaceholder() {
const placeholders = {
'A': '例如: 192.168.1.1',
'AAAA': '例如: 2001:db8::1',
'CNAME': '例如: example.com',
'MX': '例如: mail.example.com',
'TXT': '例如: 验证字符串',
'NS': '例如: ns1.example.com'
};
return placeholders[this.form.record_type] || '';
},
//
getValueDescription() {
const descriptions = {
'A': '输入IPv4地址192.168.1.1',
'AAAA': '输入IPv6地址2001:db8::1',
'CNAME': '输入目标域名example.com',
'MX': '输入邮件服务器域名mail.example.com',
'TXT': '输入文本内容,常用于域名验证',
'NS': '输入域名服务器地址ns1.example.com'
};
return descriptions[this.form.record_type] || '';
},
//
validateForm() {
if (!this.form.record_type) {
toast.error('请选择记录类型');
return false;
}
if (!this.form.value) {
toast.error('请输入记录值');
return false;
}
if (this.form.ttl < 60 || this.form.ttl > 86400) {
toast.error('TTL值必须在60-86400秒之间');
return false;
}
if (this.form.record_type === 'MX' && (this.form.mx_priority < 0 || this.form.mx_priority > 65535)) {
toast.error('MX优先级必须在0-65535之间');
return false;
}
return true;
},
//
async submitForm() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
//
const recordData = {
id: this.record.id,
type: this.form.record_type,
host: this.form.host || '@',
value: this.form.value,
ttl: this.form.ttl
};
// MX
if (this.form.record_type === 'MX') {
recordData.level = this.form.mx_priority;
}
const request = createResource({
url: 'jcloud.api.domain_west.west_domain_modify_dns',
params: {
domain: this.domainDoc.domain,
records: [recordData]
},
onSuccess: (response) => {
this.loading = false;
if (response.status === 'success') {
toast.success('DNS记录更新成功');
this.$emit('close');
if (this.onSuccess) {
this.onSuccess();
}
} else {
toast.error(response.message || '更新DNS记录失败');
}
},
onError: (error) => {
this.loading = false;
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
this.loading = false;
toast.error('更新DNS记录失败');
console.error('更新DNS记录失败:', error);
}
}
},
mounted() {
this.initForm();
}
};
</script>

View File

@ -174,6 +174,15 @@ export default {
props: domain => {
return { domain: domain.pg?.name };
}
},
{
label: '域名解析',
route: 'dns',
type: 'Component',
component: defineAsyncComponent(() => import('../components/JsiteDomainDNSRecords.vue')),
props: domain => {
return { domain: domain.pg?.name };
}
}
],
fields: [

View File

@ -312,7 +312,7 @@ class WestDomain:
return self._make_request('/domain/?act=modifydns', 'POST', body_params=body_params)
def add_dns_record(self, domain: str, record_type: str,
host: str, value: str, ttl: int = 600) -> Dict[str, Any]:
host: str, value: str, ttl: int = 600, level: int = 10) -> Dict[str, Any]:
"""
添加DNS记录
@ -322,6 +322,7 @@ class WestDomain:
host: 主机记录
value: 记录值
ttl: TTL值
level: 优先级MX记录使用
"""
record = {
'type': record_type,
@ -329,6 +330,11 @@ class WestDomain:
'value': value,
'ttl': ttl,
}
# 如果是MX记录添加优先级
if record_type == 'MX':
record['level'] = level
return self.modify_dns_records(domain, [record])
def delete_dns_record(self, domain: str, record_id: str) -> Dict[str, Any]:
@ -829,7 +835,23 @@ def west_domain_modify_dns(**data):
if not records:
return {"status": "error", "message": "缺少DNS记录参数"}
return client.modify_dns_records(domain, records)
# 处理记录数据确保MX记录有正确的优先级字段
processed_records = []
for record in records:
processed_record = {
'type': record.get('type'),
'host': record.get('host'),
'value': record.get('value'),
'ttl': record.get('ttl', 600)
}
# 如果是MX记录添加优先级
if record.get('type') == 'MX':
processed_record['level'] = record.get('level', 10)
processed_records.append(processed_record)
return client.modify_dns_records(domain, processed_records)
@jingrow.whitelist()
@ -844,11 +866,12 @@ def west_domain_add_dns_record(**data):
host = data.get('host')
value = data.get('value')
ttl = data.get('ttl', 600)
level = data.get('level', 10)
if not all([domain, record_type, host, value]):
return {"status": "error", "message": "缺少必要参数"}
return client.add_dns_record(domain, record_type, host, value, ttl)
return client.add_dns_record(domain, record_type, host, value, ttl, level)
@jingrow.whitelist()