jcloud/dashboard/src2/components/JsiteDomainDNSRecords.vue

603 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="space-y-6">
<!-- 标题和操作按钮 -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-medium text-gray-900">DNS解析记录</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="addNewRow"
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="!loading && !error" 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-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<input type="checkbox" class="rounded border-gray-300" />
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
编号
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
主机名 *
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
类型 *
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
线路
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
对应值 *
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
TTL
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
优先级
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
状态
</th>
<th class="px-4 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, index) in dnsRecords" :key="record.id || `temp-${index}`" class="hover:bg-gray-50">
<td class="px-4 py-4 whitespace-nowrap">
<input type="checkbox" class="rounded border-gray-300" />
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
{{ index + 1 }}
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 主机名编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<input
v-model="record.item"
type="text"
class="w-24 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="主机名"
/>
</div>
<div v-else class="text-sm text-gray-900">
{{ record.item || '-' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 类型编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<select
v-model="record.type"
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CNAME">CNAME</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
<option value="SRV">SRV</option>
</select>
</div>
<div v-else>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-green-100 text-green-800': record.type === 'A',
'bg-blue-100 text-blue-800': record.type === 'AAAA',
'bg-yellow-100 text-yellow-800': record.type === 'CNAME',
'bg-purple-100 text-purple-800': record.type === 'MX',
'bg-gray-100 text-gray-800': record.type === 'TXT',
'bg-orange-100 text-orange-800': record.type === 'SRV',
'bg-gray-100 text-gray-800': !record.type
}">
{{ record.type || '未知' }}
</span>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 线路编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<select
v-model="record.line"
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">默认</option>
<option value="LTEL">电信</option>
<option value="LCNC">联通</option>
<option value="LMOB">移动</option>
<option value="LEDU">教育网</option>
<option value="LSEO">搜索引擎</option>
</select>
</div>
<div v-else class="text-sm text-gray-500">
{{ getLineDisplayName(record.line) }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 对应值编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<input
v-model="record.value"
type="text"
class="w-32 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="记录值"
/>
</div>
<div v-else class="text-sm text-gray-900 max-w-xs truncate" :title="record.value">
{{ record.value || '-' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- TTL编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<input
v-model="record.ttl"
type="number"
min="60"
max="86400"
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div v-else class="text-sm text-gray-500">
{{ record.ttl || '600' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 优先级编辑 -->
<div v-if="record.editing" class="flex items-center space-x-2">
<input
v-model="record.level"
type="number"
min="1"
max="100"
class="w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div v-else class="text-sm text-gray-500">
{{ record.type === 'MX' && record.level ? record.level : '-' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
正常
</span>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm">
<div class="flex items-center space-x-1">
<Button
v-if="!record.editing"
@click="editRecord(record)"
variant="ghost"
size="sm"
class="text-blue-600 hover:text-blue-700"
>
</Button>
<Button
v-if="record.editing"
@click="saveRecord(record)"
variant="ghost"
size="sm"
class="text-green-600 hover:text-green-700"
>
保存
</Button>
<Button
v-if="record.editing"
@click="cancelEdit(record)"
variant="ghost"
size="sm"
class="text-gray-600 hover:text-gray-700"
>
取消
</Button>
<Button
v-if="!record.editing && !record.isNew"
@click="deleteRecord(record)"
variant="ghost"
size="sm"
class="text-red-600 hover:text-red-700"
>
删除
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 批量操作 -->
<div class="flex items-center justify-between bg-gray-50 px-4 py-3 rounded-md">
<div class="flex items-center space-x-4">
<input type="checkbox" class="rounded border-gray-300" />
<span class="text-sm text-gray-700">已选 0 </span>
<div class="flex items-center space-x-2">
<Button variant="outline" size="sm">批量暂停</Button>
<Button variant="outline" size="sm">批量启用</Button>
<Button variant="outline" size="sm">批量修改</Button>
<Button variant="outline" size="sm">批量删除</Button>
</div>
</div>
<div class="flex items-center space-x-4">
<Button variant="link" size="sm">导出解析记录</Button>
<Button variant="link" size="sm">导入解析记录</Button>
</div>
</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-if="!loading && !error && dnsRecords.length === 0" 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="addNewRow" 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 || []).map(record => ({
...record,
editing: false,
isNew: false
}));
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';
},
// 获取线路显示名称
getLineDisplayName(line) {
const lineMap = {
'LTEL': '电信',
'LCNC': '联通',
'LMOB': '移动',
'LEDU': '教育网',
'LSEO': '搜索引擎',
'': '默认'
};
return lineMap[line] || line;
},
// 添加新行
addNewRow() {
this.dnsRecords.push({
id: null, // 新增记录没有ID
item: '',
type: 'A',
line: '',
value: '',
ttl: 600,
level: 10,
editing: true, // 新增记录直接进入编辑模式
isNew: true
});
},
// 编辑记录
editRecord(record) {
// 保存原始值用于取消编辑
record._original = { ...record };
record.editing = true;
record.isNew = false; // 确保不是新增记录
},
// 保存记录
async saveRecord(record) {
if (!record.item || !record.type || !record.value) {
toast.error('主机名、类型和记录值不能为空');
return;
}
// 验证TTL值
if (record.ttl < 60 || record.ttl > 86400) {
toast.error('TTL值必须在60~86400秒之间');
return;
}
// 验证优先级
if (record.level < 1 || record.level > 100) {
toast.error('优先级必须在1~100之间');
return;
}
try {
let url, params;
if (record.isNew) {
// 新增记录
url = 'jcloud.api.domain_west.west_domain_add_dns_record';
params = {
domain: this.$domain.pg.domain,
record_type: record.type,
host: record.item,
value: record.value,
ttl: record.ttl,
level: record.level,
line: record.line
};
} else {
// 修改记录
url = 'jcloud.api.domain_west.west_domain_modify_dns_record';
params = {
domain: this.$domain.pg.domain,
record_id: record.id,
value: record.value,
ttl: record.ttl,
level: record.level,
line: record.line
};
}
const request = createResource({
url: url,
params: params,
onSuccess: (response) => {
if (response.status === 'success') {
toast.success('DNS记录保存成功');
record.editing = false;
record.isNew = false;
// 重新加载记录列表
this.loadDNSRecords();
} else {
toast.error(response.message || '保存DNS记录失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('保存DNS记录失败');
console.error('保存DNS记录失败:', error);
}
},
// 取消编辑
cancelEdit(record) {
if (record.isNew) {
// 如果是新增记录,直接删除
const index = this.dnsRecords.indexOf(record);
if (index > -1) {
this.dnsRecords.splice(index, 1);
}
} else {
// 如果是编辑中的记录,恢复其原始值
if (record._original) {
Object.assign(record, record._original);
delete record._original;
}
record.editing = false;
}
},
// 删除记录
deleteRecord(record) {
confirmDialog({
title: '删除DNS记录',
message: `确定要删除这条DNS记录吗\n类型: ${record.type}\n主机记录: ${record.item}\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: (response) => {
if (response.status === 'success') {
toast.success('DNS记录删除成功');
hide();
this.loadDNSRecords();
} else {
toast.error(response.message || '删除DNS记录失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('删除DNS记录失败');
console.error('删除DNS记录失败:', error);
}
}
},
computed: {
$domain() {
return getCachedDocumentResource('Jsite Domain', this.domain);
}
},
mounted() {
this.loadDNSRecords();
}
};
</script>