Files
dnsmgr/app/view/domain/smartparse.html
T
2026-05-02 20:49:09 +08:00

826 lines
24 KiB
HTML
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.
{extend name="common/layout" /}
{block name="title"}智能批量添加{/block}
{block name="main"}
<style>
.modal-body .form-group {
margin-bottom: 15px;
}
.batch-input-area {
min-height: 200px;
resize: vertical;
}
.batch-preview {
max-height: 300px;
overflow-y: auto;
margin-top: 20px;
border: 1px solid #e0e0e0;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.batch-preview table {
width: 100%;
border-collapse: collapse;
background: #fff;
margin-bottom: 0;
}
.batch-preview th,
.batch-preview td {
padding: 8px 10px;
text-align: left;
border: 0.5px solid #f0f0f0;
vertical-align: middle;
font-size: 13px;
white-space: nowrap;
}
.batch-preview th {
background-color: #f9f9f9;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e0e0e0;
position: sticky;
top: 0;
z-index: 10;
}
.batch-preview tr:hover {
background-color: #fafafa;
}
.batch-preview .label {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
}
.batch-preview .label-primary {
background-color: #337ab7;
color: #fff;
}
.batch-preview .status-success {
color: #52c41a;
font-weight: 600;
font-size: 12px;
}
.domain-select-modal {
max-height: 400px;
overflow-y: auto;
}
.domain-item {
cursor: pointer;
padding: 8px 12px;
margin-bottom: 4px;
border: 2px solid #ddd;
border-radius: 3px;
transition: all 0.2s;
}
.domain-item:hover {
border-color: #337ab7;
background-color: #f5f9fc;
}
.domain-item.selected {
border-color: #337ab7;
background-color: #e7f3ff;
}
.domain-item.selected::after {
content: '✓';
float: right;
color: #337ab7;
font-weight: bold;
}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><a href="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>智能批量添加解析</h3>
</div>
<div class="panel-body">
<form class="form-horizontal" id="batchForm">
<div class="form-group">
<label class="col-sm-3 control-label">批量数据 <span class="text-danger">*</span></label>
<div class="col-sm-6">
<textarea class="form-control batch-input-area" id="batchInput" rows="10"
placeholder="请按以下格式输入(每行一条记录):&#10;格式1:主机记录 记录值&#10;格式2:主机记录 记录值 域名&#10;格式3:记录值 主机记录.域名&#10;格式4:主机记录.域名(使用下方记录值)&#10;&#10;示例:&#10;www 1.2.3.4 example.com&#10;api app.example.com example.com&#10;1.1.1.1 www.example.com&#10;example.com&#10;&#10;说明:&#10;- 如果使用格式4,将使用下方的记录值&#10;- 如果不指定域名,将使用下方选择的默认域名&#10;- 如果检测到多个不同域名,会提示您选择对应的DNS配置"></textarea>
<p class="help-block">每行一条记录,支持混合输入多个域名的记录</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">记录值</label>
<div class="col-sm-6">
<input type="text" class="form-control" id="batchValueInput" placeholder="当使用格式4时,将使用此记录值">
<p class="help-block">留空则不使用格式4</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">默认域名</label>
<div class="col-sm-6">
<select name="defaultDomain" id="defaultDomainSelect" class="form-control select2">
<option value="">不使用默认域名(必须每行都指定域名)</option>
{foreach $domainList as $domain}
<option value="{$domain.id}">{$domain.name} [{$domain.dnsType}]</option>
{/foreach}
</select>
<p class="help-block">当某行没有指定域名时,使用此默认域名</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">记录类型</label>
<div class="col-sm-6">
<select name="defaultType" id="defaultTypeSelect" class="form-control">
<option value="">自动检测</option>
<option value="A">A</option>
<option value="CNAME">CNAME</option>
<option value="AAAA">AAAA</option>
<option value="NS">NS</option>
<option value="MX">MX</option>
<option value="SRV">SRV</option>
<option value="TXT">TXT</option>
<option value="CAA">CAA</option>
</select>
<p class="help-block">留空则根据记录值自动判断类型</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">线路</label>
<div class="col-sm-6" id="batch_line_list">
<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">
<option value="">自动选择</option>
</select>
<p class="help-block">留空则使用默认线路</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">TTL</label>
<div class="col-sm-6">
<input type="number" class="form-control" name="defaultTtl" id="defaultTtlInput" value="600" min="1">
<p class="help-block">默认TTL时间(秒)</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6">
<button type="button" class="btn btn-info" onclick="previewBatchData()"><i class="fa fa-eye"></i> 预览解析结果</button>
<button type="button" class="btn btn-primary" id="btnBatchAdd" onclick="submitBatchData()"><i class="fa fa-plus-circle"></i> 批量添加解析</button>
<button type="button" class="btn btn-default" onclick="resetBatchForm()"><i class="fa fa-refresh"></i> 重置</button>
</div>
</div>
</form>
<div class="form-group col-sm-12" id="previewSection" style="display:none;margin-top:20px;">
<label>解析预览</label>
<div class="table-responsive batch-preview">
<table style="min-width: 800px;">
<thead>
<tr>
<th style="width:5%">序号</th>
<th style="width:15%">主机记录</th>
<th style="width:10%">类型</th>
<th style="width:25%">记录值</th>
<th style="width:18%">DNS域名</th>
<th style="width:12%">线路</th>
<th style="width:8%">TTL</th>
<th style="width:7%">状态</th>
</tr>
</thead>
<tbody id="previewBody">
</tbody>
</table>
</div>
<div class="alert alert-info" id="previewSummary" style="margin-top:10px;"></div>
</div>
</div>
</div>
</div>
<div class="modal" id="modal-domain-select" role="dialog" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog modal-md">
<div class="modal-content animated flipInX">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">选择DNS配置</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i> 检测到多个不同的域名,请为每个域名选择对应的DNS配置:
</div>
<div class="domain-select-modal" id="domainSelectModal">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="confirmDomainSelection()">确定</button>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="/static/js/layer/layer.js"></script>
<script src="/static/js/select2-4.0.13.min.js"></script>
<script>
var domainList = [];
{foreach $domainList as $domain}
domainList.push({
id: '{$domain.id}',
name: '{$domain.name}',
dnsType: '{$domain.dnsType}'
});
{/foreach}
var parsedBatchData = [];
var domainMapping = {};
$(document).ready(function(){
$('#defaultDomainSelect').select2({
placeholder: '选择默认域名',
allowClear: true,
width: '100%',
language: {
noResults: function(){ return '未找到匹配的域名'; },
searching: function(){ return '搜索中...'; }
}
});
$('#defaultDomainSelect').on('change', function(){
var domainId = $(this).val();
if(domainId){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/quickinfo/' + domainId,
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
var lineOptions = '<option value="">自动选择</option>';
var firstOption = null;
$.each(data.data.recordLine, function(index, item){
if(item.parent == null){
if(!firstOption) firstOption = item.id;
lineOptions += '<option value="'+item.id+'">'+item.name+'</option>';
}
});
$('#batch_line_list').html('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">'+lineOptions+'</select>');
window.currentRecordLine = data.data.recordLine;
if(firstOption){
$('#defaultLineSelect').val(firstOption).trigger('change');
}
}else{
layer.alert(data.msg, {icon: 2});
}
},
error : function() {
layer.close(ii);
layer.alert('获取域名信息失败', {icon: 2});
}
});
}
});
});
function changeBatchLine(obj){
var line = $(obj).val();
var flag = false;
$("#batch_line_list").children().each(function(index, elem){
if(flag) $(elem).remove()
if(obj == elem){ flag = true; }
})
if($(obj).find("option:selected").text() == '子集线路(非必填)') return;
if(window.currentRecordLine){
var tempLine = window.currentRecordLine.filter((x) => x.parent == line)
if(tempLine.length > 0){
var option = line.substr(0,2) == 'N.' ? '' : '<option value="'+line+'">子集线路(非必填)</option>';
$.each(tempLine, function(index, item){
option += '<option value="'+item.id+'">'+item.name+'</option>';
})
$("#batch_line_list").append('<select name="defaultLine" class="form-control" onchange="changeBatchLine(this)">'+option+'</select>');
}
}
}
function resetBatchForm(){
$('#batchInput').val('');
$('#batchValueInput').val('');
$('#defaultDomainSelect').val(null).trigger('change');
$('#defaultTypeSelect').val('');
$('#defaultTtlInput').val(600);
$('#defaultLineSelect').val('').trigger('change');
$('#batch_line_list').empty();
$('#batch_line_list').append('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)"><option value="">自动选择</option></select>');
$('#previewSection').hide();
parsedBatchData = [];
domainMapping = {};
}
function previewBatchData(){
var inputText = $('#batchInput').val().trim();
if(!inputText){
layer.alert('请输入批量解析数据', {icon: 2});
return;
}
var lines = inputText.split('\n');
var defaultDomainId = $('#defaultDomainSelect').val();
var defaultType = $('#defaultTypeSelect').val();
var defaultLine = $('#batch_line_list select[name=defaultLine]').last().val() || $('#defaultLineSelect').val();
var defaultTtl = $('#defaultTtlInput').val();
var batchValue = $('#batchValueInput').val();
parsedBatchData = [];
domainMapping = {};
var uniqueDomains = new Set();
var errors = [];
$.each(lines, function(index, line){
line = $.trim(line);
if(!line) return;
var parts = line.split(/\s+/);
if(parts.length == 1 && batchValue){
var domainPart = parts[0];
var found = false;
var host = '@';
var domainName = domainPart;
var sortedDomains = domainList.slice().sort(function(a, b){
return b.name.length - a.name.length;
});
$.each(sortedDomains, function(i, domain){
var dnsDomainName = domain.name;
if(domainPart === dnsDomainName){
host = '@';
domainName = domain.name;
found = true;
return false;
}
else if(domainPart.endsWith('.' + dnsDomainName)){
host = domainPart.substring(0, domainPart.length - (dnsDomainName.length + 1));
domainName = domain.name;
found = true;
return false;
}
});
if(!found){
errors.push('第' + (index + 1) + '行:域名 "' + domainPart + '" 不在你的域名列表中');
return;
}
value = batchValue;
} else if(parts.length < 2){
errors.push('第' + (index + 1) + '行格式错误:至少需要主机记录和记录值');
return;
} else if(parts.length == 2){
var hostDomainPart = parts[1];
var found = false;
var sortedDomains = domainList.slice().sort(function(a, b){
return b.name.length - a.name.length;
});
$.each(sortedDomains, function(i, domain){
var dnsDomainName = domain.name;
if(hostDomainPart.endsWith('.' + dnsDomainName)){
host = hostDomainPart.substring(0, hostDomainPart.length - (dnsDomainName.length + 1));
value = parts[0];
domainName = domain.name;
found = true;
return false;
}
});
if(!found){
$.each(domainList, function(i, domain){
if(hostDomainPart === domain.name){
host = '@';
value = parts[0];
domainName = domain.name;
found = true;
return false;
}
});
if(!found){
host = parts[0];
value = parts[1];
domainName = parts[2] || null;
}
}
} else if(parts.length >= 2){
host = parts[0];
value = parts[1];
domainName = parts[2] || null;
}
var finalDomainId;
var finalDomainName;
if(domainName){
finalDomainName = domainName;
var foundDomain = null;
$.each(domainList, function(i, d){
if(d.name.toLowerCase() === domainName.toLowerCase()){
foundDomain = d;
return false;
}
});
if(foundDomain){
finalDomainId = foundDomain.id;
domainMapping[domainName] = foundDomain.id;
}else{
errors.push('第' + (index + 1) + '行:域名 "' + domainName + '" 不在你的域名列表中');
return;
}
}else if(defaultDomainId){
finalDomainId = defaultDomainId;
var defaultDomainObj = null;
$.each(domainList, function(i, d){
if(d.id === defaultDomainId){
defaultDomainObj = d;
return false;
}
});
finalDomainName = defaultDomainObj ? defaultDomainObj.name : '';
}else{
errors.push('第' + (index + 1) + '行:未指定域名且没有设置默认域名');
return;
}
uniqueDomains.add(finalDomainName);
var type = defaultType;
if(!type){
type = getDnsType(value);
}
parsedBatchData.push({
host: host,
value: value,
type: type,
domainId: finalDomainId,
domainName: finalDomainName,
line: defaultLine,
ttl: defaultTtl,
lineNumber: index + 1,
status: 'pending'
});
});
if(errors.length > 0){
layer.alert('发现以下错误:\n\n' + errors.join('\n'), {icon: 2});
return;
}
if(parsedBatchData.length === 0){
layer.alert('没有有效的解析记录', {icon: 2});
return;
}
var uniqueDnsTypes = new Set();
var domainDnsMap = {};
$.each(parsedBatchData, function(index, row){
var domainInfo = null;
$.each(domainList, function(i, d){
if(d.id === row.domainId){
domainInfo = d;
return false;
}
});
if(domainInfo){
uniqueDnsTypes.add(domainInfo.dnsType);
domainDnsMap[row.domainId] = domainInfo.dnsType;
}
});
var uniqueDomainIds = new Set(parsedBatchData.map(r => r.domainId));
if(uniqueDomainIds.size > 1){
showDomainSelectionModal(Array.from(uniqueDomainIds));
return;
}
renderPreview();
}
function getDnsType(value){
value = value.toLowerCase();
if(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value)){
return 'A';
}else if(/^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(value)){
return 'CNAME';
}else if(/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/.test(value)){
return 'AAAA';
}else if(/^\d+$/.test(value) && parseInt(value) <= 65535){
return 'MX';
}else{
return 'A';
}
}
function renderPreview(){
var html = '';
var validCount = 0;
$.each(parsedBatchData, function(index, row){
var statusHtml = '<span class="status-success"><i class="fa fa-check"></i> 待添加</span>';
validCount++;
html += '<tr>';
html += '<td style="text-align:center;">' + row.lineNumber + '</td>';
html += '<td>' + (row.host == '@' ? '@ (主域名)' : row.host) + '</td>';
html += '<td style="text-align:center;"><span class="label label-primary">' + row.type + '</span></td>';
html += '<td title="' + htmlEscape(row.value) + '">' + row.value + '</td>';
html += '<td><strong>' + row.domainName + '</strong></td>';
html += '<td style="text-align:center;">' + (row.line ? row.line : '默认') + '</td>';
html += '<td style="text-align:center;">' + row.ttl + '</td>';
html += '<td style="text-align:center;">' + statusHtml + '</td>';
html += '</tr>';
});
$('#previewBody').html(html);
$('#previewSummary').html('<strong>共 ' + validCount + ' 条记录待添加</strong>');
$('#previewSection').show();
}
function showDomainSelectionModal(domains){
var html = '';
$.each(domains, function(index, domainIdentifier){
var domainName = '';
var domainId = '';
if(!isNaN(domainIdentifier)){
var domainInfo = null;
$.each(domainList, function(i, d){
if(d.id === domainIdentifier){
domainInfo = d;
return false;
}
});
if(domainInfo){
domainName = domainInfo.name;
domainId = domainInfo.id;
}
}else{
domainName = domainIdentifier;
var domainInfo = null;
$.each(domainList, function(i, d){
if(d.name === domainIdentifier){
domainInfo = d;
return false;
}
});
if(domainInfo){
domainId = domainInfo.id;
}
}
if(!domainName) return;
var matches = [];
$.each(domainList, function(i, d){
if(d.name === domainName){
matches.push(d);
}
});
if(matches.length === 0){
matches = domainList;
}
html += '<div style="margin-bottom:20px;">';
html += '<h5><strong>' + domainName + '</strong></h5>';
html += '<div class="row">';
$.each(matches, function(j, match){
var isSelected = j === 0;
html += '<div class="col-md-6">';
html += '<div class="domain-item' + (isSelected ? ' selected' : '') + '" ';
html += 'data-domain="' + domainName + '" data-id="' + match.id + '" ';
html += 'onclick="selectDomainItem(this)">';
html += '<strong>' + match.name + '</strong> [' + match.dnsType + ']';
html += '</div>';
html += '</div>';
if(isSelected){
domainMapping[domainName] = match.id;
}
});
html += '</div></div>';
});
$('#domainSelectModal').html(html);
$('#modal-domain-select').modal('show');
}
function selectDomainItem(element){
var $element = $(element);
var domainName = $element.data('domain');
var domainId = $element.data('id');
var $row = $element.closest('.row');
$row.find('.domain-item').removeClass('selected');
$element.addClass('selected');
domainMapping[domainName] = domainId;
}
function confirmDomainSelection(){
$('#modal-domain-select').modal('hide');
$.each(parsedBatchData, function(index, row){
if(domainMapping[row.domainName]){
row.domainId = domainMapping[row.domainName];
}
});
renderPreview();
layer.msg('域名配置已更新', {icon: 1, time: 1500});
}
function submitBatchData(){
if(parsedBatchData.length === 0){
layer.alert('请先预览解析结果', {icon: 2});
return;
}
layer.confirm('确定要批量添加这 <strong>' + parsedBatchData.length + '</strong> 条解析记录吗?', {
title: '确认批量添加',
icon: 0,
btn: ['确定添加', '取消']
}, function(){
executeBatchAdd();
});
}
function executeBatchAdd(){
var groupedByDomain = {};
$.each(parsedBatchData, function(index, row){
if(!groupedByDomain[row.domainId]){
groupedByDomain[row.domainId] = [];
}
groupedByDomain[row.domainId].push(row);
});
var totalSuccess = 0;
var totalFail = 0;
var completedCount = 0;
var totalCount = Object.keys(groupedByDomain).length;
var failReasons = [];
var $btn = $('#btnBatchAdd');
var btnOrigHtml = $btn.html();
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 正在添加...');
var ii = layer.load(2);
$.each(groupedByDomain, function(domainId, records){
var recordLines = [];
$.each(records, function(i, r){
recordLines.push(r.host + ' ' + r.value);
});
var recordStr = recordLines.join('\n');
$.ajax({
type : 'POST',
url : '/record/batchadd/' + domainId,
data : function(){
var data = {
record: recordStr,
type: records[0].type,
ttl: records[0].ttl
};
if(records[0].line){
data.line = records[0].line;
}
return data;
}(),
dataType : 'json',
async: false,
success : function(data) {
completedCount++;
if(data.code == 0){
var match = data.msg.match(/成功(\d+)条/);
if(match){
totalSuccess += parseInt(match[1]);
}
var failMatch = data.msg.match(/失败(\d+)条/);
if(failMatch){
var failCount = parseInt(failMatch[1]);
totalFail += failCount;
if(failCount > 0){
var startIndex = records.length - failCount;
for(var i = startIndex; i < records.length; i++){
var record = records[i];
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']' + data.msg);
}
}
} else if(data.msg.indexOf('失败') !== -1){
failReasons.push('域名 ' + records[0].domainName + '' + data.msg);
}
}else{
totalFail += records.length;
$.each(records, function(i, record){
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']' + data.msg);
});
}
if(completedCount >= totalCount){
layer.close(ii);
$btn.prop('disabled', false).html(btnOrigHtml);
var msg = '批量添加完成!';
if(totalSuccess > 0){
msg += '\n成功:' + totalSuccess + ' 条';
}
if(totalFail > 0){
msg += '\n失败:' + totalFail + ' 条';
if(failReasons.length > 0){
msg += '\n\n失败原因:';
$.each(failReasons, function(i, reason){
msg += '\n' + (i + 1) + '. ' + reason;
});
}
}
layer.alert(msg, {
icon: totalFail > 0 ? 2 : 1,
btn: ['确定'],
yes: function(index){
layer.close(index);
try {
resetBatchForm();
} catch(e) {
console.error('Error in callback:', e);
}
}
});
}
},
error : function() {
completedCount++;
totalFail += records.length;
$.each(records, function(i, record){
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:网络错误,无法连接服务器');
});
if(completedCount >= totalCount){
layer.close(ii);
$btn.prop('disabled', false).html(btnOrigHtml);
var msg = '批量添加完成!';
if(totalSuccess > 0){
msg += '\n成功:' + totalSuccess + ' 条';
}
if(totalFail > 0){
msg += '\n失败:' + totalFail + ' 条';
if(failReasons.length > 0){
msg += '\n\n失败原因:';
$.each(failReasons, function(i, reason){
msg += '\n' + (i + 1) + '. ' + reason;
});
}
}
layer.alert(msg, {
icon: totalFail > 0 ? 2 : 1,
btn: ['确定'],
yes: function(index){
layer.close(index);
try {
resetBatchForm();
} catch(e) {
console.error('Error in callback:', e);
}
}
});
}
}
});
});
}
function htmlEscape(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
</script>
{/block}