jcloud/dashboard/src2/components/JsiteDomainDNSRecords.vue

794 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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>
</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
v-if="selectedRecords.length > 0"
@click="batchDeleteRecords"
variant="outline"
size="sm"
class="text-red-600 hover:text-red-700"
>
<Trash2Icon class="h-4 w-4 mr-1" />
删除选中 ({{ selectedRecords.length }})
</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"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
/>
</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"
:checked="selectedRecords.includes(String(record.id))"
@change="toggleRecordSelection(record.id)"
:disabled="record.isNew"
/>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
{{ record.isNew ? '-' : (dnsRecords.filter(r => !r.isNew).findIndex(r => r.id === record.id) + 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"
:class="{ 'bg-gray-100 text-gray-500 cursor-not-allowed': !record.isNew }"
placeholder="主机名"
:disabled="!record.isNew"
/>
</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"
@change="handleRecordTypeChange(record)"
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"
:class="{ 'bg-gray-100 text-gray-500 cursor-not-allowed': !record.isNew }"
:disabled="!record.isNew"
>
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CNAME">CNAME</option>
<option value="MX">MX</option>
<option value="NS">NS</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-indigo-100 text-indigo-800': record.type === 'NS',
'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 && (record.type === 'MX' || record.type === 'SRV')" 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.type === 'SRV') && 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 v-if="pagination.total > pagination.limit" class="flex flex-col items-center space-y-4">
<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"
class="px-3"
>
</Button>
<div class="flex items-center space-x-1">
<Button
v-for="page in getVisiblePages()"
:key="page"
@click="changePage(page)"
:class="[
'px-3 py-1 text-sm rounded',
page === pagination.pageno
? '!bg-[#1fc76f] text-white'
: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
]"
:disabled="page === pagination.pageno"
>
{{ page }}
</Button>
</div>
<Button
@click="changePage(pagination.pageno + 1)"
:disabled="pagination.pageno >= pagination.pagecount"
variant="outline"
size="sm"
class="px-3"
>
</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';
import SettingsIcon from '~icons/lucide/settings';
export default {
name: 'JsiteDomainDNSRecords',
props: ['domain'],
data() {
return {
loading: false,
error: null,
dnsRecords: [],
pagination: {
pageno: 1,
limit: 10,
total: 0,
pagecount: 0
},
selectedRecords: [] // 用于存储选中的记录ID
};
},
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',
'NS': 'info',
'TXT': 'secondary',
'SRV': 'warning'
};
return variantMap[type] || 'default';
},
// 获取线路显示名称
getLineDisplayName(line) {
const lineMap = {
'LTEL': '电信',
'LCNC': '联通',
'LMOB': '移动',
'LEDU': '教育网',
'LSEO': '搜索引擎',
'': '默认'
};
return lineMap[line] || line;
},
// 添加新行
addNewRow() {
const newRecord = {
id: null, // 新增记录没有ID
item: '',
type: 'A',
line: '',
value: '',
ttl: 600,
level: null, // 默认不设置优先级
editing: true, // 新增记录直接进入编辑模式
isNew: true
};
// 添加新记录到最前面
this.dnsRecords.unshift(newRecord);
// 如果默认类型是MX或SRV设置默认优先级
this.handleRecordTypeChange(newRecord);
},
// 处理记录类型变化时的优先级设置
handleRecordTypeChange(record) {
if (record.type === 'MX' || record.type === 'SRV') {
// 如果是MX或SRV类型且优先级为空则设置为默认值10
if (!record.level) {
record.level = 10;
}
} else {
// 其他类型不设置优先级
record.level = null;
}
},
// 编辑记录
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;
}
// 验证优先级 - 只在MX或SRV类型时验证
if ((record.type === 'MX' || record.type === 'SRV') && (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,
line: record.line
};
// 只在MX或SRV类型时添加level参数
if (record.type === 'MX' || record.type === 'SRV') {
params.level = record.level;
}
} 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,
line: record.line
};
// 只在MX或SRV类型时添加level参数
if (record.type === 'MX' || record.type === 'SRV') {
params.level = record.level;
}
}
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: 'solid',
class: 'bg-black text-white hover:bg-gray-800',
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);
}
},
// 批量删除记录
async batchDeleteRecords() {
if (this.selectedRecords.length === 0) {
toast.info('请选择要删除的记录');
return;
}
confirmDialog({
title: '批量删除DNS记录',
message: `确定要删除选中的 ${this.selectedRecords.length} 条DNS记录吗`,
primaryAction: {
label: '确定',
variant: 'solid',
class: 'bg-black text-white hover:bg-gray-800',
onClick: ({ hide }) => {
this.performBatchDeleteRecords(this.selectedRecords, hide);
}
}
});
},
// 执行批量删除记录
async performBatchDeleteRecords(recordIds, hide) {
// 过滤出有效的记录ID
const validRecordIds = recordIds
.filter(id => id)
.map(id => String(id))
.filter(id => !id.startsWith('temp-'));
if (validRecordIds.length === 0) {
toast.error('没有有效的记录可以删除');
return;
}
try {
const request = createResource({
url: 'jcloud.api.domain_west.west_domain_delete_dns_records',
params: {
domain: this.$domain.pg.domain,
record_ids: validRecordIds
},
onSuccess: (response) => {
if (response.status === 'success') {
toast.success(response.message || `批量删除 ${validRecordIds.length} 条DNS记录成功`);
hide();
this.loadDNSRecords();
this.selectedRecords = []; // 清空选中
} else if (response.status === 'partial_success') {
toast.success(response.message || '批量删除部分成功');
hide();
this.loadDNSRecords();
this.selectedRecords = []; // 清空选中
} else {
toast.error(response.message || '批量删除DNS记录失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('批量删除DNS记录失败');
console.error('批量删除DNS记录失败:', error);
}
},
// 切换记录选中状态
toggleRecordSelection(recordId) {
// 只允许选择有效的记录ID排除新增记录和空ID
if (!recordId) {
return;
}
// 确保recordId是字符串类型
const recordIdStr = String(recordId);
if (recordIdStr.startsWith('temp-')) {
return;
}
const index = this.selectedRecords.indexOf(recordIdStr);
if (index > -1) {
this.selectedRecords.splice(index, 1);
} else {
this.selectedRecords.push(recordIdStr);
}
},
// 全选/取消全选
toggleSelectAll(event) {
const checkbox = event.target;
if (checkbox.checked) {
// 只选择有真实ID的记录排除新增的记录
this.selectedRecords = this.dnsRecords
.filter(record => record.id && !record.isNew)
.map(record => String(record.id));
} else {
this.selectedRecords = [];
}
},
// 获取可见的页码
getVisiblePages() {
const current = this.pagination.pageno;
const total = this.pagination.pagecount;
const pages = [];
// 如果总页数小于等于5显示所有页码
if (total <= 5) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
// 显示当前页附近的页码
const start = Math.max(1, current - 2);
const end = Math.min(total, current + 2);
for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
}
},
computed: {
$domain() {
return getCachedDocumentResource('Jsite Domain', this.domain);
},
// 判断是否全选
isAllSelected() {
return this.dnsRecords.length > 0 && this.selectedRecords.length === this.dnsRecords.length;
},
// 判断是否半选
isIndeterminate() {
return this.selectedRecords.length > 0 && this.selectedRecords.length < this.dnsRecords.length;
}
},
mounted() {
this.loadDNSRecords();
}
};
</script>