1.修复已有解析记录:修改清空搜索,切换域名没清空搜索,还有显示问题 2.Cloudflare自定义主机名添加CF优选解析和批量CF优选解析 (#456)

* Add files via upload

1.修复已有解析记录:修改清空搜索,切换域名没清空搜索,还有显示问题
2.Cloudflare自定义主机名添加CF优选解析和批量CF优选解析

* Add files via upload
This commit is contained in:
wmwlwmwl
2026-04-29 23:10:31 +08:00
committed by GitHub
parent a1cfd470d9
commit a99e3b8642
3 changed files with 934 additions and 9 deletions

View File

@@ -432,7 +432,26 @@ class Cloudflare extends BaseController
throw new Exception('解析线路列表为空');
}
return json(['code' => 0, 'data' => ['default_line' => strval($firstKey)]]);
$lines = [];
foreach ($recordLine as $lineValue => $lineLabel) {
if (is_array($lineLabel)) {
$lines[] = [
'value' => strval($lineValue),
'label' => isset($lineLabel['name']) ? strval($lineLabel['name']) : strval($lineValue),
'parent' => isset($lineLabel['parent']) ? ($lineLabel['parent'] !== null ? strval($lineLabel['parent']) : '') : '',
'is_default' => ($lineValue === $firstKey)
];
} else {
$lines[] = [
'value' => strval($lineValue),
'label' => strval($lineLabel),
'parent' => '',
'is_default' => ($lineValue === $firstKey)
];
}
}
return json(['code' => 0, 'data' => ['default_line' => strval($firstKey), 'lines' => $lines]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}

View File

@@ -59,6 +59,7 @@
<button type="button" class="btn btn-default btn-sm" onclick="batchHostnameTxtVerification()" disabled id="btnBatchHostnameTxt"><i class="fa fa-check-circle"></i> 批量主机名验证</button>
<button type="button" class="btn btn-default btn-sm" onclick="batchCertTxtVerification()" disabled id="btnBatchCertTxt"><i class="fa fa-certificate"></i> 批量证书验证</button>
<button type="button" class="btn btn-success btn-sm" onclick="batchRefreshVerification()" disabled id="btnBatchRefresh"><i class="fa fa-refresh"></i> 批量刷新验证</button>
<button type="button" class="btn btn-warning btn-sm" onclick="batchCfOptimized()" disabled id="btnBatchCfOptimized"><i class="fa fa-bolt"></i> 批量CF优选解析</button>
<button type="button" class="btn btn-warning btn-sm" onclick="batchEditHostnames()" disabled id="btnBatchEdit"><i class="fa fa-pencil"></i> 批量修改</button>
<button type="button" class="btn btn-danger btn-sm" onclick="batchDeleteHostnames()" disabled id="btnBatchDelete"><i class="fa fa-trash"></i> 批量删除</button>
</div>
@@ -248,6 +249,55 @@
</div>
</div>
<div class="modal" id="modal-cf-optimized" role="dialog" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content animated flipInX">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
<h4 class="modal-title" id="cfOptimizedModalTitle">CF 优选解析</h4>
</div>
<div class="modal-body">
<div class="alert alert-info" id="batchCfOptimizedAlert"></div>
<div id="batchCfOptimizedList" style="max-height:400px;overflow-y:auto;"></div>
<div class="form-group" style="margin-top:12px;">
<label>CNAME 目标 <span style="color:#d9534f;">*</span></label>
<div id="cfOptimizedTargetList"></div>
</div>
<div id="cfOptimizedDnsGroupArea" style="display:none;">
<!-- 每个DNS域名一组主机名列表 + DNS服务商选择 -->
</div>
<div id="cfOptimizedSingleDnsArea" style="display:none;" class="form-group">
<label>选择解析服务商 <span style="color:#d9534f;">*</span></label>
<select id="cfOptimizedDnsProvider" class="form-control">
<option value="">请选择</option>
</select>
</div>
<div id="singleLineArea" style="display:none;" class="form-group">
<label>解析线路</label>
<select id="singleLineSelect" class="form-control cf-line-main">
<option value="">请先选择解析服务商</option>
</select>
</div>
<div id="singleSubLineArea" style="display:none;" class="form-group">
<label>子线路</label>
<select id="singleSubLineSelect" class="form-control">
<option value="">请选择</option>
</select>
</div>
<div id="cfOptimizedExistingRecord" style="display:none;" class="alert alert-warning">
<strong>检测到已存在记录:</strong><br>
<span id="cfOptimizedExistingInfo"></span><br>
<span style="font-size:12px;color:#666;">点击确认将修改为新的 CNAME 目标</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" onclick="confirmBatchCfOptimized()" id="cfOptimizedConfirmBtn">确定</button>
</div>
</div>
</div>
</div>
<div class="modal" id="modal-dcv-target-picker" role="dialog" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content animated flipInX">
@@ -448,6 +498,7 @@ $(document).ready(function(){
return ''
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">验证</a> '
+ '<a href="javascript:openCfOptimizedForHostname(\''+htmlEscape(row.hostname)+'\')" class="btn btn-warning btn-xs">CF优选解析</a> '
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
}
}
@@ -469,6 +520,7 @@ function updateBatchButtons(){
$("#btnBatchHostnameTxt").prop('disabled', !hasSelection);
$("#btnBatchCertTxt").prop('disabled', !hasSelection);
$("#btnBatchRefresh").prop('disabled', !hasSelection);
$("#btnBatchCfOptimized").prop('disabled', !hasSelection);
}
function filterHostnameList(searchText){
@@ -1483,7 +1535,844 @@ function confirmBatchRefresh(){
});
}
// 提交批量 TXT 记录的通用函数
// ==================== CF 优选解析 ====================
var cfOptimizedTargets = [
{value: 'www.visa.cn', label: 'Visa中国'},
{value: 'mfa.gov.ua', label: '乌克兰外交部'},
{value: 'www.shopify.com', label: 'Shopify官方'},
{value: 'store.ubi.com', label: 'Ubisoft'},
{value: 'staticdelivery.nexusmods.com', label: 'NexusMods'}
];
var cfOptimizedApiTargets = [];
function loadCfOptimizedTargetsFromApi(callback){
cfOptimizedApiTargets = [];
$.ajax({
type: 'POST',
url: '/optimizeip/opiplist/data',
data: {offset: 0, limit: 100, type: 1, status: null, kw: ''},
dataType: 'json',
success: function(res){
if(res.rows && res.rows.length > 0){
for(var i = 0; i < res.rows.length; i++){
var row = res.rows[i];
if(row.cdn_type == 1 && row.active == 1 && row.rr && row.domain){
var targetValue = row.rr + '.' + row.domain;
var targetLabel = row.remark || (row.rr + '.' + row.domain);
cfOptimizedApiTargets.push({value: targetValue, label: targetLabel});
}
}
}
if(callback) callback();
},
error: function(){
if(callback) callback();
}
});
}
function renderCfOptimizedTargetOptions(){
var allTargets = cfOptimizedApiTargets.concat(cfOptimizedTargets);
var html = '';
if(cfOptimizedApiTargets.length > 0){
html += '<div style="font-size:12px;color:#888;margin-bottom:6px;padding-bottom:4px;">— 来自 CF优选IP任务 —</div>';
}
for(var i = 0; i < allTargets.length; i++){
var opt = allTargets[i];
var isApi = i < cfOptimizedApiTargets.length;
if(!isApi && i === cfOptimizedApiTargets.length && cfOptimizedApiTargets.length > 0){
html += '<div style="border-top:1px solid #eee;margin:8px 0 4px;"></div>';
}
html += '<div class="radio" style="margin:0 0 8px;">';
html += '<label><input type="radio" name="cfOptimizedTarget" value="' + htmlEscape(opt.value) + '"' + (i === 0 ? ' checked' : '') + '> ';
html += '<strong>' + htmlEscape(opt.value) + '</strong> <span style="color:#888;font-size:12px;">(' + htmlEscape(opt.label) + ')' + (isApi ? ' <i class="fa fa-database" style="color:#337ab7;font-size:10px;" title="来自优选任务"></i>' : '') + '</span>';
html += '</label></div>';
}
html += '<div class="radio" style="margin:4px 0 0;padding-top:8px;border-top:1px solid #eee;">';
html += '<label><input type="radio" name="cfOptimizedTarget" value="_custom"> ';
html += '<strong>自定义</strong></label>';
html += '<div id="cfOptimizedCustomInput" style="display:none;margin-top:6px;margin-left:20px;">';
html += '<div class="row"><div class="col-xs-4">';
html += '<select id="cfOptimizedCustomType" class="form-control input-sm">';
html += '<option value="CNAME" selected>CNAME</option>';
html += '<option value="A">A</option>';
html += '<option value="AAAA">AAAA</option>';
html += '</select></div><div class="col-xs-8">';
html += '<input type="text" id="cfOptimizedCustomValue" class="form-control input-sm" placeholder="输入目标值域名或IP"></div></div>';
html += '</div></div>';
$('#cfOptimizedTargetList').html(html);
$('#cfOptimizedTargetList input[name=cfOptimizedTarget]').on('change', function(){
if($(this).val() === '_custom'){
$('#cfOptimizedCustomInput').show();
} else {
$('#cfOptimizedCustomInput').hide();
}
});
}
var cfOptimizedSingleHostname = null;
var batchCfOptimizedData = [];
function resetCfOptimizedState(){
var confirmBtn = $("#cfOptimizedConfirmBtn");
confirmBtn.prop('disabled', false).text('开始处理').data('cfmode', '').removeData('savedDnsDomains cfExistingId preDetectResults cfExistingRec');
$('#cfOptimizedExistingRecord').hide();
$('#cfOptimizedDnsGroupArea [id^=lineArea_]').hide();
$('#cfOptimizedDnsGroupArea [id^=subLineArea_]').hide();
$('#singleLineArea').hide();
$('#singleSubLineArea').hide();
$('#batchCfOptimizedList').html('').hide();
cfOptimizedSingleHostname = null;
for(var ci = 0; ci < batchCfOptimizedData.length; ci++){ delete batchCfOptimizedData[ci]._dnsSel; }
}
$(document).ready(function(){
$('#modal-cf-optimized').on('hidden.bs.modal', function(){
resetCfOptimizedState();
});
});
function getCfOptimizedTargetValue(){
var selectedTarget = $('input[name=cfOptimizedTarget]:checked').val();
if(!selectedTarget) return null;
if(selectedTarget === '_custom'){
var customValue = $.trim($('#cfOptimizedCustomValue').val());
var customType = $('#cfOptimizedCustomType').val() || 'CNAME';
if(!customValue) return null;
return {value: customValue, type: customType};
}
return {value: selectedTarget, type: 'CNAME'};
}
// 批量入口
function batchCfOptimized(){
resetCfOptimizedState();
var selectedRows = $("#listTable").bootstrapTable('getSelections');
if(selectedRows.length === 0){
layer.msg('请先选择要设置 CF 优选解析的自定义主机名', {icon: 0});
return;
}
$('#cfOptimizedModalTitle').text('批量 CF 优选解析');
$('#cfOptimizedConfirmBtn').text('开始处理');
loadCfOptimizedTargetsFromApi(function(){
renderCfOptimizedTargetOptions();
// 先获取所有主机名的 DNS 域名信息,按 DNS 域名分组
batchCfOptimizedData = [];
var processedCount = 0;
var totalCount = selectedRows.length;
function checkGroupDone(){
if(processedCount === totalCount){
// 按 DNS 域名分组
var dnsGroups = {};
for(var i = 0; i < batchCfOptimizedData.length; i++){
var item = batchCfOptimizedData[i];
if(!item.targets || !item.targets.length){
item.dnsDomain = '未知域名';
} else {
item.dnsDomain = item.targets[0].domain_name || '未知域名';
}
if(!dnsGroups[item.dnsDomain]){
dnsGroups[item.dnsDomain] = [];
}
dnsGroups[item.dnsDomain].push(item);
}
// 渲染分组 UI
renderCfOptimizedDnsGroups(dnsGroups, false);
$("#modal-cf-optimized").modal('show');
}
}
selectedRows.forEach(function(row){
var hostname = row.hostname;
resolveTxtTargets(hostname, '', function(targets){
processedCount++;
batchCfOptimizedData.push({row: row, hostname: hostname, targets: targets});
checkGroupDone();
});
});
});
}
// 单个入口
function openCfOptimizedForHostname(hostname){
resetCfOptimizedState();
cfOptimizedSingleHostname = hostname;
$('#cfOptimizedModalTitle').text('CF 优选解析');
$('#cfOptimizedConfirmBtn').text('确定');
loadCfOptimizedTargetsFromApi(function(){
renderCfOptimizedTargetOptions();
resolveTxtTargets(hostname, '', function(targets){
var dnsDomain = (targets && targets.length) ? (targets[0].domain_name || '未知域名') : '未知域名';
batchCfOptimizedData = [{row: {hostname: hostname}, hostname: hostname, targets: targets, dnsDomain: dnsDomain}];
var dnsGroups = {};
dnsGroups[dnsDomain] = [{row: {hostname: hostname}, hostname: hostname, targets: targets, dnsDomain: dnsDomain}];
renderCfOptimizedDnsGroups(dnsGroups, true);
$("#modal-cf-optimized").modal('show');
});
});
}
// 渲染按 DNS 域名分组的 UI
function renderCfOptimizedDnsGroups(dnsGroups, isSingle){
var groupIndex = 0;
var groupHtml = '';
for(var dnsDomain in dnsGroups){
if(!dnsGroups.hasOwnProperty(dnsDomain)) continue;
var items = dnsGroups[dnsDomain];
var hostnamesStr = items.map(function(item){ return item.hostname; }).join(', ');
// 收集该组内所有主机名的 targets去重合并选项
var allTargets = [];
var seenTargetIds = {};
for(var ti = 0; ti < items.length; ti++){
if(items[ti].targets){
for(var tj = 0; tj < items[ti].targets.length; tj++){
var t = items[ti].targets[tj];
if(t.domain_id && !seenTargetIds[t.domain_id]){
seenTargetIds[t.domain_id] = true;
allTargets.push(t);
}
}
}
}
groupHtml += '<div class="panel panel-default" style="margin-bottom:10px;">';
groupHtml += '<div class="panel-heading" style="padding:8px 12px;"><strong>DNS 域名:' + htmlEscape(dnsDomain) + '</strong></div>';
groupHtml += '<div class="panel-body" style="padding:8px 12px;">';
groupHtml += '<div style="margin-bottom:6px;font-size:13px;color:#555;">主机名:<strong>' + htmlEscape(hostnamesStr) + '</strong></div>';
groupHtml += '<select id="dnsProvider_' + groupIndex + '" data-dns-domain="' + htmlEscape(dnsDomain) + '" data-group-index="' + groupIndex + '" class="form-control input-sm cf-dns-provider">';
groupHtml += '<option value="">请选择</option>';
if(allTargets.length > 0){
allTargets.forEach(function(target){
var providerName = target.account_type_name || target.account_type || '-';
var displayName = target.domain_name + ' (' + providerName + ')';
groupHtml += '<option value="' + target.domain_id + '">' + htmlEscape(displayName) + '</option>';
});
} else {
groupHtml += '<option value="" disabled>未找到解析域名</option>';
}
groupHtml += '</select>';
groupHtml += '<div id="lineArea_' + groupIndex + '" style="margin-top:6px;display:none;"><label style="font-size:13px;color:#555;">解析线路</label>';
groupHtml += '<select id="lineSelect_' + groupIndex + '" class="form-control input-sm cf-line-main"><option value="">加载中...</option></select>';
groupHtml += '<div id="subLineArea_' + groupIndex + '" style="margin-top:4px;display:none;"><label style="font-size:12px;color:#999;">子线路</label>';
groupHtml += '<select id="subLineSelect_' + groupIndex + '" class="form-control input-sm"><option value="">请选择</option></select></div></div>';
groupHtml += '</div></div>';
groupIndex++;
}
if(Object.keys(dnsGroups).length > 1 || !isSingle){
// 多域名或批量模式:显示分组区域
$('#cfOptimizedDnsGroupArea').html(groupHtml).show();
$('#cfOptimizedSingleDnsArea').hide();
$('#batchCfOptimizedList').hide();
$('#batchCfOptimizedAlert').html('确定要为选中的 <strong>' + batchCfOptimizedData.length + '</strong> 个主机名设置 CF 优选 CNAME 解析吗?');
} else {
// 单域名单个模式:显示简化界面
$('#cfOptimizedDnsGroupArea').hide();
$('#cfOptimizedSingleDnsArea').show();
var hostnamesHtml = '';
for(var d in dnsGroups){
if(!dnsGroups.hasOwnProperty(d)) continue;
hostnamesHtml += '<div style="padding:12px;background:#f9f9f9;border-radius:4px;"><strong>' + htmlEscape(dnsGroups[d][0].hostname) + '</strong></div>';
}
$('#batchCfOptimizedList').html(hostnamesHtml).show();
$('#batchCfOptimizedAlert').html('<strong>说明:</strong>将主机名 CNAME 解析到 Cloudflare 优选 IP 域名,实现加速访问。');
// 把第一个组的选项复制到单选框
if(groupHtml){
var tempDiv = $('<div>').html(groupHtml);
var selectOptions = tempDiv.find('select:first option');
var optionsHtml = '';
selectOptions.each(function(){ optionsHtml += $(this)[0].outerHTML; });
$('#cfOptimizedDnsProvider').html(optionsHtml);
}
}
// 绑定 DNS 服务商选择事件选择后加载该域名的线路列表DOM 已渲染完毕)
function bindLineEvents(){
// 批量模式DNS 服务商选择 → 加载线路(事件委托)
$('#cfOptimizedDnsGroupArea').off('change.cfline', '.cf-dns-provider').on('change.cfline', '.cf-dns-provider', function(){
var domainId = $(this).val();
var groupIndex = $(this).data('groupIndex');
var lineArea = $('#lineArea_' + groupIndex);
var lineSelect = $('#lineSelect_' + groupIndex);
var subLineArea = $('#subLineArea_' + groupIndex);
if(!domainId){ lineArea.hide(); subLineArea.hide(); return; }
lineArea.show();
subLineArea.hide();
lineSelect.html('<option value="">加载中...</option>');
$.ajax({
type: 'POST', url: '/cloudflare/get_domain_default_line', data: {domain_id: domainId}, dataType: 'json',
success: function(res){
if(res.code === 0 && res.data && res.data.lines && res.data.lines.length > 0){
lineSelect.data('allLines', res.data.lines);
var opts = '';
for(var li = 0; li < res.data.lines.length; li++){
var l = res.data.lines[li];
if(!l.parent){
opts += '<option value="' + htmlEscape(l.value) + '"' + (l.is_default ? ' selected' : '') + '>' + htmlEscape(l.label) + '</option>';
}
}
lineSelect.html(opts || '<option value="" selected>请选择</option>');
} else {
var defLine = (res.code === 0 && res.data && res.data.default_line) ? res.data.default_line : '0';
lineSelect.html('<option value="' + htmlEscape(defLine) + '" selected>默认</option>');
lineSelect.data('allLines', []);
}
},
error: function(){ lineSelect.html('<option value="0" selected>默认</option>'); lineSelect.data('allLines', []); }
});
});
// 批量模式:主线路选择 → 显示子线路(事件委托,更可靠)
$('#cfOptimizedDnsGroupArea').off('change.cfsub', '.cf-line-main').on('change.cfsub', '.cf-line-main', function(){
var groupIndex = $(this).attr('id').replace('lineSelect_', '');
var subLineArea = $('#subLineArea_' + groupIndex);
var subLineSelect = $('#subLineSelect_' + groupIndex);
var allLines = $(this).data('allLines') || [];
var selectedVal = $(this).val();
// 找该父线路的子线路
var children = [];
for(var ci = 0; ci < allLines.length; ci++){
if(allLines[ci].parent === selectedVal){
children.push(allLines[ci]);
}
}
if(children.length > 0){
var opts = '';
for(var si = 0; si < children.length; si++){
opts += '<option value="' + htmlEscape(children[si].value) + '">' + htmlEscape(children[si].label) + '</option>';
}
subLineSelect.html(opts);
subLineArea.show();
} else {
subLineArea.hide();
}
});
// 单域名模式DNS 服务商选择 → 加载线路
$('#cfOptimizedSingleDnsArea #cfOptimizedDnsProvider').off('change.cfline').on('change.cfline', function(){
var domainId = $(this).val();
var lineArea = $('#singleLineArea');
var lineSelect = $('#singleLineSelect');
var subLineArea = $('#singleSubLineArea');
if(!domainId){ lineArea.hide(); subLineArea.hide(); return; }
lineArea.show();
subLineArea.hide();
lineSelect.html('<option value="">加载中...</option>');
$.ajax({
type: 'POST', url: '/cloudflare/get_domain_default_line', data: {domain_id: domainId}, dataType: 'json',
success: function(res){
if(res.code === 0 && res.data && res.data.lines && res.data.lines.length > 0){
lineSelect.data('allLines', res.data.lines);
var opts = '';
for(var li = 0; li < res.data.lines.length; li++){
var l = res.data.lines[li];
if(!l.parent){
opts += '<option value="' + htmlEscape(l.value) + '"' + (l.is_default ? ' selected' : '') + '>' + htmlEscape(l.label) + '</option>';
}
}
lineSelect.html(opts || '<option value="" selected>请选择</option>');
} else {
var defLine = (res.code === 0 && res.data && res.data.default_line) ? res.data.default_line : '0';
lineSelect.html('<option value="' + htmlEscape(defLine) + '" selected>默认</option>');
lineSelect.data('allLines', []);
}
},
error: function(){ lineSelect.html('<option value="0" selected>默认</option>'); lineSelect.data('allLines', []); }
});
});
// 单域名模式:主线路选择 → 显示子线路(直接绑定,因为 ID 固定)
$('#singleLineSelect').off('change.cfsub').on('change.cfsub', function(){
var subLineArea = $('#singleSubLineArea');
var subLineSelect = $('#singleSubLineSelect');
var allLines = $(this).data('allLines') || [];
var selectedVal = $(this).val();
var children = [];
for(var ci = 0; ci < allLines.length; ci++){
if(allLines[ci].parent === selectedVal){
children.push(allLines[ci]);
}
}
if(children.length > 0){
var opts = '';
for(var si = 0; si < children.length; si++){
opts += '<option value="' + htmlEscape(children[si].value) + '">' + htmlEscape(children[si].label) + '</option>';
}
subLineSelect.html(opts);
subLineArea.show();
} else {
subLineArea.hide();
}
});
}
bindLineEvents();
}
// 确认处理
function confirmBatchCfOptimized(){
var confirmBtn = $("#cfOptimizedConfirmBtn");
var targetInfo = getCfOptimizedTargetValue();
if(!targetInfo || !targetInfo.value){ layer.msg('请选择 CNAME 目标', {icon: 0}); return; }
var cnameTarget = targetInfo.value;
var recordType = targetInfo.type || 'CNAME';
// 收集所有组的选择(或使用上次已保存的选择)
var dnsDomains = {};
var allSelected = true;
var savedDomains = confirmBtn.data('savedDnsDomains');
if(savedDomains){
// 批量二次确认:使用预检测时保存的选择
dnsDomains = savedDomains;
} else if($('#cfOptimizedDnsGroupArea').is(':visible')){
// 多域名分组模式:首次收集(按域名匹配,不依赖 select 顺序)
$('#cfOptimizedDnsGroupArea select[id^=dnsProvider_]').each(function(){
var dnsDomain = $(this).data('dnsDomain') || '';
var val = $(this).val();
var domainId = val;
var groupIndex = $(this).data('groupIndex');
var lineVal = $('#lineSelect_' + groupIndex).val() || '0';
var subLineVal = $('#subLineSelect_' + groupIndex).val();
if(subLineVal) lineVal = subLineVal;
// 获取线路的显示名称(用于预检测结果显示)
var lineLabel = '';
if(subLineVal){
lineLabel = $('#subLineSelect_' + groupIndex).find('option:selected').text() || '';
} else {
lineLabel = $('#lineSelect_' + groupIndex).find('option:selected').text() || '';
}
dnsDomains[dnsDomain] = {domainId: domainId, dnsDomain: dnsDomain, line: lineVal, lineLabel: lineLabel};
if(!val) allSelected = false;
});
} else {
// 单域名模式
var domainId = $('#cfOptimizedDnsProvider').val();
if(!domainId) allSelected = false;
var singleLineVal = $('#singleLineSelect').val() || '0';
var singleSubLineVal = $('#singleSubLineSelect').val();
if(singleSubLineVal) singleLineVal = singleSubLineVal;
var singleLineLabel = '';
if(singleSubLineVal){
singleLineLabel = $('#singleSubLineSelect').find('option:selected').text() || '';
} else {
singleLineLabel = $('#singleLineSelect').find('option:selected').text() || '';
}
// 直接从数据取实际域名作为 key此时 orderedDomains 尚未构建,不能引用它)
var singleDnsKey = batchCfOptimizedData.length > 0 ? (batchCfOptimizedData[0].dnsDomain || '0') : '0';
dnsDomains[singleDnsKey] = {domainId: domainId, line: singleLineVal, lineLabel: singleLineLabel};
}
if(!allSelected){ layer.msg('请为所有 DNS 域名选择解析服务商', {icon: 0}); return; }
// 保存选择供批量二次确认时复用(此时 DOM 可能已被隐藏)
confirmBtn.data('savedDnsDomains', dnsDomains);
// 将 DNS 选择直接存到每个主机名数据上(不依赖 DOM 可见状态)
var orderedDomains = [];
var seenDomains = {};
for(var di = 0; di < batchCfOptimizedData.length; di++){
var dd = batchCfOptimizedData[di].dnsDomain;
if(dd && !seenDomains[dd]){ seenDomains[dd] = true; orderedDomains.push(dd); }
}
if(orderedDomains.length > 1){
// 多域名分组模式(按域名匹配,不依赖顺序)
for(var gi = 0; gi < orderedDomains.length; gi++){
var sel = dnsDomains[orderedDomains[gi]];
if(!sel) continue;
for(var ei = 0; ei < batchCfOptimizedData.length; ei++){
if(batchCfOptimizedData[ei].dnsDomain === orderedDomains[gi]){
batchCfOptimizedData[ei]._dnsSel = sel;
}
}
}
} else {
// 单分组模式dnsDomains key 是域名非数字索引,用 orderedDomains[0] 取正确值)
var singleSel = orderedDomains.length > 0 ? dnsDomains[orderedDomains[0]] : null;
for(var si = 0; si < batchCfOptimizedData.length; si++){
batchCfOptimizedData[si]._dnsSel = singleSel;
}
}
var btnMode = confirmBtn.data('cfmode');
// 单个模式的二次确认(检测已存在记录)
if(cfOptimizedSingleHostname && btnMode !== 'execute'){
var singleDomainKey = orderedDomains.length > 0 ? orderedDomains[0] : 0;
var sel = dnsDomains[singleDomainKey];
var singleItem = batchCfOptimizedData[0];
var singleDnsDomain = singleItem.dnsDomain || '';
var singleRecordName = cfOptimizedSingleHostname;
if(singleDnsDomain && cfOptimizedSingleHostname.length > singleDnsDomain.length && cfOptimizedSingleHostname.substr(cfOptimizedSingleHostname.length - singleDnsDomain.length - 1) === '.' + singleDnsDomain){
singleRecordName = cfOptimizedSingleHostname.substr(0, cfOptimizedSingleHostname.length - singleDnsDomain.length - 1);
} else if(cfOptimizedSingleHostname === singleDnsDomain){
singleRecordName = '@';
}
confirmBtn.prop('disabled', true).text('检测中...');
$.ajax({
type: 'POST',
url: '/record/list',
data: {id: sel.domainId, rr: singleRecordName},
dataType: 'json',
success: function(res){
confirmBtn.prop('disabled', false);
// A/CNAME/AAAA 在同一线路互斥,预检测也匹配全部三种
var allowedTypes = ['A', 'AAAA', 'CNAME'];
var existingRecord = null;
var existingInfo = '';
// 获取用户选择的线路 value 和 label
var selLine = (sel && sel.line) ? sel.line : '0';
var selLineLabel = (sel && sel.lineLabel) ? sel.lineLabel : '';
if(res.code === 0 && res.data && res.data.length > 0){
for(var i = 0; i < res.data.length; i++){
var recType = (res.data[i].Type || '').toUpperCase();
if(allowedTypes.indexOf(recType) === -1) continue;
var recLine = String(res.data[i].Line || res.data[i].line || '');
var recLineName = res.data[i].LineName || (res.data[i].line_name || '');
// 线路精确匹配:优先比较 value不匹配则用 label 兜底
var isLineMatch = (recLine === selLine);
if(!isLineMatch && selLineLabel && recLineName){
isLineMatch = (recLineName === selLineLabel);
}
if(isLineMatch){
existingRecord = res.data[i];
break;
}
}
}
if(existingRecord){
var recType = (existingRecord.Type || '').toUpperCase();
var recLineName = existingRecord.LineName || (existingRecord.line_name || String(existingRecord.Line || existingRecord.line || '默认'));
var displaySelLabel = selLineLabel || selLine || '默认';
var targetRecType = recordType || 'CNAME';
var typeChangeInfo = (recType === targetRecType) ? (recType + '') : ('目标类型:' + targetRecType + ' → 现有类型:' + recType);
existingInfo = typeChangeInfo + ' | 线路:' + recLineName + ' → 目标线路:' + displaySelLabel + ' | 值:' + (existingRecord.Value || existingRecord.value || '-');
$('#cfOptimizedExistingInfo').text(existingInfo);
$('#cfOptimizedExistingRecord').show();
confirmBtn.text('确认修改').data('cfmode', 'execute').data('cfExistingId', existingRecord.RecordId).data('cfExistingRec', existingRecord);
} else {
confirmBtn.data('cfmode', 'execute');
executeCfOptimized(cnameTarget, dnsDomains, recordType);
}
},
error: function(){
confirmBtn.prop('disabled', false).text('确定');
layer.alert('检测已有记录失败', {icon: 2});
}
});
return;
}
// 批量模式的二次确认 或 单个模式已确认
if(btnMode === 'execute' || btnMode === 'batchExecute'){
executeCfOptimized(cnameTarget, dnsDomains, recordType);
return;
}
// 批量模式第一次点击:预检测
confirmBtn.prop('disabled', true).text('检测中...');
$('#cfOptimizedExistingRecord').hide();
preDetectAllHostnames(cnameTarget, dnsDomains, recordType, function(results){
confirmBtn.prop('disabled', false);
var modifyCount = results.modifyList.length;
var addCount = results.addList.length;
var failCount = results.failList.length;
// 更新列表显示检测结果
var listHtml = '';
batchCfOptimizedData.forEach(function(item){
var h = item.hostname;
if(results.modifySet[h] && results.modifySet[h] !== true){
var rec = results.modifySet[h];
var recType = (rec.Type || '').toUpperCase();
var recLineName = rec.LineName || (rec.line_name || String(rec.Line || rec.line || '默认'));
var selLineLabel = (item._dnsSel && item._dnsSel.lineLabel) ? item._dnsSel.lineLabel : (item._dnsSel && item._dnsSel.line) || '默认';
var targetRecType = recordType || 'CNAME';
var typeInfo = (recType === targetRecType) ? ('类型:' + recType) : ('目标类型:' + targetRecType + ' → 现有类型:' + recType);
listHtml += '<div style="padding:8px;border-bottom:1px solid #eee;background:#fff3cd;">';
listHtml += '<strong>' + htmlEscape(h) + '</strong> <span style="color:#d9534f;font-size:12px;margin-left:6px;">⚠ 将修改已有记录</span>';
listHtml += '<div style="font-size:11px;color:#888;margin-top:2px;">' + typeInfo + ' | 线路:' + htmlEscape(recLineName) + ' → 目标线路:' + htmlEscape(selLineLabel) + ' | 值:' + htmlEscape(rec.Value || rec.value || '-') + '</div>';
listHtml += '</div>';
} else if(results.addSet[h]){
var addLineLabel = (item._dnsSel && item._dnsSel.lineLabel) ? item._dnsSel.lineLabel : '默认';
var addTargetType = recordType || 'CNAME';
listHtml += '<div style="padding:8px;border-bottom:1px solid #eee;background:#dff0d8;">';
listHtml += '<strong>' + htmlEscape(h) + '</strong> <span style="color:#3c763d;font-size:12px;margin-left:6px;">✓ 将新增记录</span>';
listHtml += '<div style="font-size:11px;color:#888;margin-top:2px;">目标类型:' + addTargetType + ' | 目标线路:' + htmlEscape(addLineLabel) + '</div>';
listHtml += '</div>';
} else {
listHtml += '<div style="padding:8px;border-bottom:1px solid #eee;background:#f2dede;">';
listHtml += '<strong>' + htmlEscape(h) + '</strong> <span style="color:#a94442;font-size:12px;margin-left:6px;">✗ 检测失败</span>';
listHtml += '</div>';
}
});
$('#batchCfOptimizedList').html(listHtml).show();
var summary = '';
if(modifyCount > 0) summary += '<span style="color:#d9534f;"><strong>' + modifyCount + '</strong> 个将修改已有记录</span>';
if(addCount > 0){ if(summary) summary += ' | '; summary += '<span style="color:#3c763d;"><strong>' + addCount + '</strong> 个将新增记录</span>'; }
if(failCount > 0){ if(summary) summary += ' | '; summary += '<span style="color:#a94442;"><strong>' + failCount + '</strong> 个检测失败</span>'; }
$('#batchCfOptimizedAlert').html(summary);
// 无论检测结果如何都允许继续(执行阶段会再次检测并决定添加/修改)
$('#cfOptimizedDnsGroupArea').hide();
$('#cfOptimizedSingleDnsArea').hide();
$('#cfOptimizedExistingRecord').show().removeClass('alert-warning alert-info').addClass(failCount > 0 ? 'alert-warning' : 'alert-info');
if(failCount > 0 && failCount === totalCount){
$('#cfOptimizedExistingInfo').text('检测失败的主机名将尝试直接添加记录');
} else if(failCount > 0){
$('#cfOptimizedExistingInfo').text('部分主机名检测失败,失败的将尝试直接添加。请确认后点击"确认执行"');
} else {
$('#cfOptimizedExistingInfo').text('请确认以上检测结果后点击"确认执行"');
}
confirmBtn.text('确认执行').data('cfmode', 'batchExecute');
// 保存预检测结果供后续使用
confirmBtn.data('preDetectResults', results);
});
}
// 预检测所有主机名是否有已存在记录
function preDetectAllHostnames(cnameTarget, dnsDomains, recordType, callback){
// A/CNAME/AAAA 在同一线路互斥,预检测也匹配全部三种
var allowedTypes = ['A', 'AAAA', 'CNAME'];
var detectedCount = 0;
var totalCount = batchCfOptimizedData.length;
var modifyList = [], addList = [], failList = [];
var modifySet = {}, addSet = {};
function checkDone(){
if(detectedCount === totalCount){
callback({modifyList: modifyList, addList: addList, failList: failList, modifySet: modifySet, addSet: addSet});
}
}
// 直接使用已存到每个主机名数据上的 _dnsSel
batchCfOptimizedData.forEach(function(item){
var hostname = item.hostname;
var sel = item._dnsSel;
// _dnsSel 为空时 fallback 到 targets 数据
if(!sel && item.targets && item.targets.length > 0){
sel = {domainId: item.targets[0].domain_id};
}
if(!sel || !sel.domainId){ failList.push(hostname); detectedCount++; if(detectedCount === totalCount) checkDone(); return; }
// 直接从主机名和 DNS 域名计算 record_name不依赖 targets 中的值,避免误判)
var dnsDomain = item.dnsDomain || '';
var recordName = hostname;
if(dnsDomain && hostname.length > dnsDomain.length && hostname.substr(hostname.length - dnsDomain.length - 1) === '.' + dnsDomain){
recordName = hostname.substr(0, hostname.length - dnsDomain.length - 1);
} else if(hostname === dnsDomain){
recordName = '@';
}
if(!recordName) recordName = hostname;
detectedCount++;
(function(hn, rn, s){
// 获取用户选择的线路 value 和 labellabel 用于兜底匹配)
var selLine = s.line || '0';
var selLineLabel = s.lineLabel || '';
$.ajax({
type: 'POST',
url: '/record/list',
data: {id: s.domainId, rr: rn},
dataType: 'json',
success: function(res){
var existingRecord = null;
if(res.code === 0 && res.data && res.data.length > 0){
for(var i = 0; i < res.data.length; i++){
var recType = (res.data[i].Type || '').toUpperCase();
if(allowedTypes.indexOf(recType) === -1) continue;
var recLine = String(res.data[i].Line || res.data[i].line || '');
var recLineName = res.data[i].LineName || (res.data[i].line_name || '');
// 线路精确匹配:优先比较 value不匹配则用 label 兜底
var isLineMatch = (recLine === selLine);
if(!isLineMatch && selLineLabel && recLineName){
isLineMatch = (recLineName === selLineLabel);
}
if(isLineMatch){
existingRecord = res.data[i];
break;
}
}
}
if(existingRecord){
modifyList.push(hn);
modifySet[hn] = existingRecord;
} else {
addList.push(hn);
addSet[hn] = true;
}
checkDone();
},
error: function(){
failList.push(hn);
checkDone();
}
});
})(hostname, recordName, sel);
});
}
// 执行 CF 优选操作
function executeCfOptimized(cnameTarget, dnsDomains, recordType){
var confirmBtn = $("#cfOptimizedConfirmBtn");
confirmBtn.prop('disabled', true).text('处理中...');
var ii = layer.load(2);
var batchResults = {success: 0, failed: 0, errors: []};
var processedCount = 0;
var totalCount = batchCfOptimizedData.length;
var execRecordType = recordType || 'CNAME';
// 执行阶段A/CNAME/AAAA 在同一线路互斥,找到任一种都走修改路径(用用户选择的类型覆盖)
var allowedTypes = ['A', 'AAAA', 'CNAME'];
function checkCompletion(){
if(processedCount === totalCount){
layer.close(ii);
resetCfOptimizedState();
$("#modal-cf-optimized").modal('hide');
var msg = 'CF 优选解析完成:成功 ' + batchResults.success + ' 个,失败 ' + batchResults.failed + ' 个';
if(batchResults.errors.length > 0) msg += '\n\n失败详情\n' + batchResults.errors.join('\n');
layer.alert(msg, {icon: batchResults.failed > 0 ? 2 : 1});
}
}
// 处理单个模式的预存 RecordId
var singlePendingId = null;
if(cfOptimizedSingleHostname && confirmBtn.data('cfExistingId')){
singlePendingId = confirmBtn.data('cfExistingId');
cfOptimizedSingleHostname = null;
}
function processOneItem(item, line){
var hostname = item.hostname;
var sel = item._dnsSel;
// _dnsSel 为空时 fallback 到 targets 数据
if(!sel && item.targets && item.targets.length > 0){
sel = {domainId: item.targets[0].domain_id, line: line};
}
// 直接从主机名和 DNS 域名计算 record_name
var dnsDomain = item.dnsDomain || '';
var recordName = hostname;
if(dnsDomain && hostname.length > dnsDomain.length && hostname.substr(hostname.length - dnsDomain.length - 1) === '.' + dnsDomain){
recordName = hostname.substr(0, hostname.length - dnsDomain.length - 1);
} else if(hostname === dnsDomain){
recordName = '@';
}
if(!recordName) recordName = hostname;
if(!sel || !sel.domainId){
batchResults.failed++; batchResults.errors.push(hostname + ': 无匹配的DNS服务商'); processedCount++; checkCompletion(); return;
}
// 如果有预存的单个模式 RecordId 且是同一个主机名,直接使用
var useExistingId = singlePendingId && batchCfOptimizedData.length === 1;
if(useExistingId){
submitOneRecord(sel.domainId, recordName, cnameTarget, line, singlePendingId, hostname, batchResults, execRecordType, function(){
processedCount++; checkCompletion();
});
return;
}
// 否则检测后提交(按用户选择的线路匹配)
$.ajax({
type: 'POST',
url: '/record/list',
data: {id: sel.domainId, rr: recordName},
dataType: 'json',
success: function(res){
var existingRecordId = null;
if(res.code === 0 && res.data && res.data.length > 0){
for(var i = 0; i < res.data.length; i++){
var recType = (res.data[i].Type || '').toUpperCase();
var recLine = String(res.data[i].Line || res.data[i].line || '');
if(allowedTypes.indexOf(recType) !== -1 && recLine === line){
existingRecordId = res.data[i].RecordId; break;
}
}
}
submitOneRecord(sel.domainId, recordName, cnameTarget, line, existingRecordId, hostname, batchResults, execRecordType, function(){
processedCount++; checkCompletion();
});
},
error: function(){
batchResults.failed++; batchResults.errors.push(hostname + ': 检测已有记录失败'); processedCount++; checkCompletion();
}
});
}
function submitOneRecord(domainId, recordName, cnameTarget, line, existingRecordId, hostname, br, recType, cb){
var postData = {name: recordName, type: (recType || 'CNAME'), value: cnameTarget, line: line, ttl: 600, mx: 1, weight: 0, remark: 'Cloudflare 优选解析'};
if(existingRecordId) postData.recordid = existingRecordId;
var url = existingRecordId ? ('/record/update/' + domainId) : ('/record/add/' + domainId);
var actionMsg = existingRecordId ? '修改' : '添加';
$.ajax({
type: 'POST', url: url, data: postData, dataType: 'json',
success: function(res){
if(res.code === 0){
br.success++;
} else {
if(existingRecordId && res.code !== 0){
var retryData = {name: recordName, type: (recType || 'CNAME'), value: cnameTarget, line: line, ttl: 600, mx: 1, weight: 0, remark: 'Cloudflare 优选解析'};
$.ajax({
type: 'POST', url: '/record/add/' + domainId, data: retryData, dataType: 'json',
success: function(retryRes){
if(retryRes.code === 0){ br.success++; } else { br.failed++; br.errors.push(hostname + ': 添加失败 - ' + (retryRes.msg || '')); }
cb();
},
error: function(){ br.failed++; br.errors.push(hostname + ': 添加网络错误'); cb(); }
});
return;
}
br.failed++;
br.errors.push(hostname + ': ' + actionMsg + '失败 - ' + (res.msg || ''));
}
cb();
},
error: function(){
br.failed++; br.errors.push(hostname + ': 网络错误'); cb();
}
});
}
// 每个主机名使用自己组选择的线路,直接处理
batchCfOptimizedData.forEach(function(item){
var itemLine = (item._dnsSel && item._dnsSel.line) ? item._dnsSel.line : '0';
processOneItem(item, itemLine);
});
}
function submitBatchTxtRecord(txtName, txtValue, target, remark, callback){
// 先获取目标域名的默认线路
$.ajax({

View File

@@ -78,14 +78,10 @@
}
.record-table .action-cell {
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
text-align: center;
}
.record-table .action-cell .btn {
margin: 0;
margin: 2px 1px;
}
.no-records {
text-align: center;
@@ -649,6 +645,10 @@ var currentPage = 1;
var pageSize = 20;
var totalRecords = 0;
// 搜索状态
var isSearching = false;
var searchKeyword = '';
// 域名列表(用于批量添加时匹配)
var domainList = [];
{foreach $domainList as $domain}
@@ -744,6 +744,11 @@ $(document).ready(function(){
$('#domainSelect').on('change', function(){
var domainId = $(this).val();
if(domainId){
// 清空搜索状态
isSearching = false;
searchKeyword = '';
$('#searchKeyword').val('');
loadDomainInfo(domainId);
currentPage = 1;
loadRecordList();
@@ -1477,6 +1482,12 @@ function loadRecordList(){
var domainId = $('#domainSelect').val();
if(!domainId) return;
// 如果处于搜索状态,执行搜索
if(isSearching && searchKeyword){
searchRecords();
return;
}
var ii = layer.load(2);
$.ajax({
type : 'POST',
@@ -1519,7 +1530,7 @@ function renderRecordList(records){
html += '<tr data-index="' + index + '">';
html += '<td style="text-align:center;"><input type="checkbox" class="record-checkbox" value="' + row.RecordId + '" data-info="' + recordInfoStr + '" onchange="updateSelectedCount()"></td>';
html += '<td>' + (row.Name == '@' ? '@ (主域名)' : row.Name) + '</td>';
html += '<td style="vertical-align: top;">' + (row.Name == '@' ? '@ (主域名)' : row.Name) + '</td>';
html += '<td><span class="label label-primary">' + row.Type + '</span></td>';
html += '<td class="record-value" title="' + htmlEscape(valueDisplay) + '">';
html += '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#' + copyId + '\')" style="padding-right:6px;"><i class="fa fa-copy"></i></a>';
@@ -1621,11 +1632,17 @@ function searchRecords(){
if(!keyword){
// 无搜索词时,恢复正常加载
isSearching = false;
searchKeyword = '';
currentPage = 1;
loadRecordList();
return;
}
// 设置搜索状态
isSearching = true;
searchKeyword = keyword;
// 获取所有记录进行搜索(分页获取所有数据)
var ii = layer.load(2);
var allData = [];