mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-02 11:56:27 +02:00
* Add files via upload 1.修复已有解析记录:修改清空搜索,切换域名没清空搜索,还有显示问题 2.Cloudflare自定义主机名添加CF优选解析和批量CF优选解析 * Add files via upload
3422 lines
138 KiB
HTML
3422 lines
138 KiB
HTML
{extend name="common/layout" /}
|
||
{block name="title"}Cloudflare自定义主机名 - {$domainName}{/block}
|
||
{block name="main"}
|
||
<div class="row">
|
||
<div class="col-xs-12 center-block" style="float:none;">
|
||
<div class="panel panel-default">
|
||
<div class="panel-heading" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px;">
|
||
<h3 class="panel-title" style="margin: 0;">Cloudflare自定义主机名 - {$domainName}</h3>
|
||
<div>
|
||
<a href="/record/{$domainId}" class="btn btn-sm btn-default" title="返回解析"><i class="fa fa-reply fa-fw"></i> 返回解析</a>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="alert alert-info">
|
||
<strong>说明:</strong> 这里管理 Cloudflare 自定义主机名、证书状态、证书验证与 Fallback Origin。
|
||
</div>
|
||
|
||
<div class="well well-sm">
|
||
<div class="form-inline" style="display:flex;flex-wrap:wrap;gap:10px;align-items:flex-start;">
|
||
<div class="form-group" style="width:100%;max-width:720px;">
|
||
<label style="display:block;margin-bottom:8px;">Fallback Origin</label>
|
||
<input type="text" id="fallbackOrigin" class="form-control" style="width:100%;" placeholder="例如 origin.example.com">
|
||
</div>
|
||
<div style="width:100%;display:flex;gap:8px;flex-wrap:wrap;">
|
||
<button type="button" class="btn btn-primary flex-1" onclick="saveFallbackOrigin()">保存</button>
|
||
<button type="button" class="btn btn-default flex-1" onclick="loadFallbackOrigin()">刷新</button>
|
||
<button type="button" class="btn btn-danger flex-1" onclick="clearFallbackOrigin()">清空</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="well well-sm">
|
||
<div class="form-inline" style="display:flex;flex-wrap:wrap;gap:10px;align-items:flex-start;">
|
||
<div class="form-group" style="width:100%;max-width:720px;">
|
||
<label style="display:block;margin-bottom:8px;">DCV 委派</label>
|
||
<input type="text" id="dcvDelegationUuid" class="form-control" style="width:100%;" placeholder="获取中..." readonly>
|
||
</div>
|
||
<div style="width:100%;display:flex;gap:8px;flex-wrap:wrap;">
|
||
<button type="button" class="btn btn-success flex-1" id="btnQuickAddDcv" onclick="quickAddDcvDelegation()" disabled title="需要先获取到 UUID"><i class="fa fa-magic"></i> 一键添加CNAME</button>
|
||
<button type="button" class="btn btn-primary flex-1" onclick="loadDcvDelegationUuid()">刷新</button>
|
||
<button type="button" class="btn btn-default flex-1" onclick="showDcvDelegationHelp()">帮助</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel panel-default margin-top-10">
|
||
<div class="panel-heading">
|
||
<h4 class="panel-title">自定义主机名列表</h4>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div id="toolbar" style="margin-bottom: 10px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
|
||
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
|
||
<input type="text" id="hostnameSearchInput" class="form-control input-sm" placeholder="搜索主机名..." oninput="filterHostnameList(this.value)" style="max-width: 300px;">
|
||
</div>
|
||
<a href="javascript:refreshHostnameList()" class="btn btn-default btn-sm" title="刷新自定义主机名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||
<a href="javascript:openAddDialog()" class="btn btn-success btn-sm"><i class="fa fa-plus"></i> 添加自定义主机名</a>
|
||
<a href="javascript:batchAddHostnames()" class="btn btn-primary btn-sm"><i class="fa fa-plus-circle"></i> 批量添加</a>
|
||
<button type="button" class="btn btn-info btn-sm" onclick="batchDcvDelegation()" disabled id="btnBatchDcv"><i class="fa fa-link"></i> 批量 DCV 委派</button>
|
||
<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>
|
||
<table id="listTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-store" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title" id="storeTitle">添加自定义主机名</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="form-horizontal" id="form-store">
|
||
<input type="hidden" name="hostname_id" value="">
|
||
<div class="form-group">
|
||
<label class="col-sm-3 control-label">主机名</label>
|
||
<div class="col-sm-9">
|
||
<input type="text" class="form-control" name="hostname" placeholder="例如 app.example.com 或 *.example.com" required>
|
||
<p class="help-block" id="hostnameHint">创建后主机名不能直接改名,如需改名请删除后重建。</p>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="col-sm-3 control-label">自定义源站</label>
|
||
<div class="col-sm-9">
|
||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||
<p class="help-block">留空表示清空当前自定义源站,回退到 Fallback Origin 或默认源站逻辑。</p>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="col-sm-3 control-label">证书验证方法</label>
|
||
<div class="col-sm-9">
|
||
<select class="form-control" name="ssl_method">
|
||
<option value="txt">TXT 验证(推荐)</option>
|
||
<option value="http">HTTP 验证</option>
|
||
</select>
|
||
<p class="help-block">选择验证方法后,系统将根据选择生成相应的验证信息。</p>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="col-sm-3 control-label">最低 TLS 版本</label>
|
||
<div class="col-sm-9">
|
||
<select class="form-control" name="min_tls_version">
|
||
<option value="1.0">TLS 1.0(默认)</option>
|
||
<option value="1.1">TLS 1.1</option>
|
||
<option value="1.2">TLS 1.2</option>
|
||
<option value="1.3">TLS 1.3</option>
|
||
</select>
|
||
<p class="help-block">设置此自定义主机名区域的最低 TLS 版本。</p>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||
<button type="button" class="btn btn-primary" onclick="submitHostname()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-dcv-help" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">DCV 委派帮助</h4>
|
||
</div>
|
||
<div class="modal-body" id="dcvHelpContent"></div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-delete" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量删除确认</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-danger" id="batchDeleteAlert"></div>
|
||
<div id="batchDeleteHostnameList" style="max-height:200px;overflow-y:auto;"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-danger" onclick="confirmBatchDelete()">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-dcv" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量 DCV 委派</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" id="batchDcvAlert"></div>
|
||
<div id="batchDcvHostnameList" style="max-height:400px;overflow-y:auto;"></div>
|
||
<div class="alert alert-warning" style="margin-top:12px;">
|
||
<strong>注意:</strong>此操作将为选中的自定义主机名创建 DCV 委派 CNAME 记录,每条记录将在您选择的解析域名中添加。
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-info" onclick="confirmBatchDcv()">开始批量添加</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-hostname-txt" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量主机名 TXT 验证</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" id="batchHostnameTxtAlert"></div>
|
||
<div id="batchHostnameTxtList" style="max-height:400px;overflow-y:auto;"></div>
|
||
<div class="alert alert-warning" style="margin-top:12px;">
|
||
<strong>注意:</strong>此操作将为选中的自定义主机名创建主机名 TXT 验证记录,每条记录将在您选择的解析域名中添加。
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-success" onclick="confirmBatchHostnameTxt()">开始添加</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-cert-txt" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量证书 TXT 验证</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" id="batchCertTxtAlert"></div>
|
||
<div id="batchCertTxtList" style="max-height:400px;overflow-y:auto;"></div>
|
||
<div class="alert alert-warning" style="margin-top:12px;">
|
||
<strong>注意:</strong>此操作将为选中的自定义主机名创建证书 TXT 验证记录,每条记录将在您选择的解析域名中添加。
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-success" onclick="confirmBatchCertTxt()">开始添加</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-refresh" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量刷新验证</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" id="batchRefreshAlert"></div>
|
||
<div id="batchRefreshList" style="max-height:400px;overflow-y:auto;"></div>
|
||
<div class="alert alert-warning" style="margin-top:12px;">
|
||
<strong>注意:</strong>此操作将向 Cloudflare 重新发起验证请求,可能会触发证书重新签发。请确认要为选中的主机名执行此操作。
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-success" onclick="confirmBatchRefresh()">开始刷新</button>
|
||
</div>
|
||
</div>
|
||
</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>×</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">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">选择解析服务商 - DCV 委派</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-warning" style="margin-bottom:12px;">检测到多个可用解析域名,请确认要写入哪个服务商。</div>
|
||
<div class="form-group"><label>CNAME 主机名</label><div id="dcvTargetHostname"></div></div>
|
||
<div class="form-group"><label>CNAME 目标</label><textarea id="dcvTargetValue" class="form-control" rows="2" readonly></textarea></div>
|
||
<div id="dcvTargetPickerList"></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="confirmDcvTargetSelection()">添加 CNAME</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-edit" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title" id="batchEditTitle">批量修改自定义主机名</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" id="batchEditAlert"></div>
|
||
<form id="batchEditForm">
|
||
<input type="hidden" name="hostname_ids" value="">
|
||
<div class="form-group">
|
||
<label>自定义源站</label>
|
||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||
<p class="help-block">留空表示清空当前自定义源站,回退到 Fallback Origin 或默认源站逻辑</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>证书验证方法</label>
|
||
<select class="form-control" name="ssl_method">
|
||
<option value="">保持不变</option>
|
||
<option value="txt">TXT 验证(推荐)</option>
|
||
<option value="http">HTTP 验证</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>最低 TLS 版本</label>
|
||
<select class="form-control" name="min_tls_version">
|
||
<option value="">保持不变</option>
|
||
<option value="1.0">TLS 1.0(默认)</option>
|
||
<option value="1.1">TLS 1.1</option>
|
||
<option value="1.2">TLS 1.2</option>
|
||
<option value="1.3">TLS 1.3</option>
|
||
</select>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="submitBatchEdit()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-batch-add" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">批量添加自定义主机名</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info">批量添加自定义主机名,每行一个主机名</div>
|
||
<form id="batchAddForm">
|
||
<div class="form-group">
|
||
<label>主机名列表</label>
|
||
<textarea class="form-control" name="hostnames" rows="10" placeholder="每行输入一个主机名,例如: app.example.com api.example.com *.subdomain.example.com"></textarea>
|
||
<p class="help-block">支持通配符主机名,如 *.example.com</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>自定义源站</label>
|
||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||
<p class="help-block">留空表示使用 Fallback Origin 或默认源站逻辑</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>证书验证方法</label>
|
||
<select class="form-control" name="ssl_method">
|
||
<option value="txt">TXT 验证(推荐)</option>
|
||
<option value="http">HTTP 验证</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>最低 TLS 版本</label>
|
||
<select class="form-control" name="min_tls_version">
|
||
<option value="1.0">TLS 1.0(默认)</option>
|
||
<option value="1.1">TLS 1.1</option>
|
||
<option value="1.2">TLS 1.2</option>
|
||
<option value="1.3">TLS 1.3</option>
|
||
</select>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="submitBatchAdd()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-dcv-selector" role="dialog" aria-hidden="true" data-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content animated flipInX">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||
<h4 class="modal-title">选择域名 - DCV 委派</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info" style="margin-bottom:12px;font-size:14px;">请选择要为哪个自定义主机名添加 DCV 委派 CNAME 记录。</div>
|
||
<div class="form-group" style="margin-bottom:12px;">
|
||
<input type="text" id="dcvHostnameSearch" class="form-control" placeholder="搜索自定义主机名..." oninput="filterDcvHostnamesModal(this.value)">
|
||
</div>
|
||
<div id="dcvHostnameList" style="max-height:400px;overflow-y:auto;padding-right:8px;">
|
||
<form id="hostnameSelectorForm"></form>
|
||
</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="confirmDcvHostnameSelection()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal-verification" 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>×</span></button>
|
||
<h4 class="modal-title" id="verificationTitle">证书验证</h4>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="verificationContent"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-primary" onclick="refreshHostnameValidation()">刷新验证</button>
|
||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/block}
|
||
{block name="script"}
|
||
<script src="/static/js/layer/layer.js"></script>
|
||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||
<script src="/static/js/custom.js?v=1005"></script>
|
||
<script>
|
||
var currentVerificationHostnameId = '';
|
||
var allHostnameData = []; // 保存完整的原始数据用于客户端搜索
|
||
var domainId = '{$domainId}'; // 域名 ID
|
||
|
||
$(document).ready(function(){
|
||
$("#form-store").bootstrapValidator();
|
||
loadFallbackOrigin();
|
||
loadDcvDelegationUuid();
|
||
$("#listTable").bootstrapTable({
|
||
url: '/cloudflare/hostnames/data/{$domainId}',
|
||
method: 'post',
|
||
toolbar: '#toolbar',
|
||
classes: 'table table-striped table-hover table-bordered',
|
||
uniqueId: 'id',
|
||
sidePagination: 'client',
|
||
pagination: true,
|
||
pageSize: 20,
|
||
pageList: [10, 20, 50, 100],
|
||
queryParams: function(params){ return {}; },
|
||
responseHandler: hostnameResponseHandler,
|
||
columns: [
|
||
{field: 'checkbox', checkbox: true, width: '50px'},
|
||
{field: 'hostname', title: '主机名'},
|
||
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
||
{field: 'ssl_method', title: '验证方法', formatter: function(v){ return v || '-'; }},
|
||
{field: 'ssl_min_tls_version', title: '最低 TLS 版本', formatter: function(v){ return v || '-'; }},
|
||
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
||
{field: 'ssl_validation_status', title: '证书验证', formatter: formatStatus},
|
||
{field: 'verification_status', title: '主机名验证', formatter: formatStatus},
|
||
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
||
{
|
||
field: 'action',
|
||
title: '操作',
|
||
formatter: function(value, row){
|
||
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>';
|
||
}
|
||
}
|
||
]
|
||
});
|
||
|
||
// 添加复选框事件监听
|
||
$("#listTable").on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table', function(){
|
||
updateBatchButtons();
|
||
});
|
||
});
|
||
|
||
function updateBatchButtons(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
var hasSelection = selectedRows.length > 0;
|
||
$("#btnBatchDelete").prop('disabled', !hasSelection);
|
||
$("#btnBatchEdit").prop('disabled', !hasSelection);
|
||
$("#btnBatchDcv").prop('disabled', !hasSelection);
|
||
$("#btnBatchHostnameTxt").prop('disabled', !hasSelection);
|
||
$("#btnBatchCertTxt").prop('disabled', !hasSelection);
|
||
$("#btnBatchRefresh").prop('disabled', !hasSelection);
|
||
$("#btnBatchCfOptimized").prop('disabled', !hasSelection);
|
||
}
|
||
|
||
function filterHostnameList(searchText){
|
||
var searchTextLower = (searchText || '').toLowerCase();
|
||
var filteredData = [];
|
||
|
||
if(searchTextLower === ''){
|
||
// 搜索为空时,显示所有原始数据
|
||
filteredData = allHostnameData;
|
||
} else {
|
||
// 基于原始数据进行过滤
|
||
for(var i = 0; i < allHostnameData.length; i++){
|
||
var row = allHostnameData[i];
|
||
if(row && row.hostname && row.hostname.toLowerCase().indexOf(searchTextLower) !== -1){
|
||
filteredData.push(row);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用 load 方法重新加载过滤后的数据(客户端分页)
|
||
$('#listTable').bootstrapTable('load', filteredData);
|
||
}
|
||
|
||
var batchDeleteHostnamesData = [];
|
||
|
||
function batchDeleteHostnames(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
if(selectedRows.length === 0){
|
||
layer.msg('请先选择要删除的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
batchDeleteHostnamesData = selectedRows;
|
||
var hostnames = selectedRows.map(function(row){ return '<div>' + htmlEscape(row.hostname) + '</div>'; }).join('');
|
||
|
||
$('#batchDeleteAlert').text('确定要删除选中的 ' + selectedRows.length + ' 个自定义主机名吗?');
|
||
$('#batchDeleteHostnameList').html(hostnames);
|
||
$("#modal-batch-delete").modal('show');
|
||
}
|
||
|
||
function confirmBatchDelete(){
|
||
var ii = layer.load(2);
|
||
var hostnameIds = batchDeleteHostnamesData.map(function(row){ return row.id; });
|
||
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/batch_delete/{$domainId}',
|
||
data: {hostname_ids: hostnameIds},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#modal-batch-delete").modal('hide');
|
||
layer.msg('批量删除成功', {icon: 1, time: 1500});
|
||
refreshHostnameList();
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
var batchDcvHostnamesData = [];
|
||
|
||
function batchDcvDelegation(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
if(selectedRows.length === 0){
|
||
layer.msg('请先选择要添加 DCV 委派的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 检查是否已获取 DCV 委派 UUID
|
||
var uuid = $.trim($('#dcvDelegationUuid').val());
|
||
if(!uuid){
|
||
layer.msg('请先获取 DCV 委派 UUID', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
batchDcvHostnamesData = selectedRows;
|
||
|
||
// 先获取所有主机名的 DNS 信息,按实际 DNS 域名分组
|
||
var hostnamesWithDns = [];
|
||
var processedCount = 0;
|
||
var totalCount = selectedRows.length;
|
||
|
||
selectedRows.forEach(function(row, index){
|
||
var hostname = row.hostname;
|
||
var cnameFullName = '_acme-challenge.' + hostname;
|
||
|
||
resolveDcvCnameTargets(cnameFullName, function(targets){
|
||
processedCount++;
|
||
|
||
if(targets.length > 0){
|
||
// 使用第一个目标域名作为该主机名的 DNS 域名
|
||
var dnsDomain = targets[0].domain_name;
|
||
hostnamesWithDns.push({
|
||
row: row,
|
||
hostname: hostname,
|
||
dnsDomain: dnsDomain,
|
||
targets: targets
|
||
});
|
||
} else {
|
||
// 无法解析 DNS 域名的主机名
|
||
hostnamesWithDns.push({
|
||
row: row,
|
||
hostname: hostname,
|
||
dnsDomain: '未知域名',
|
||
targets: []
|
||
});
|
||
}
|
||
|
||
if(processedCount === totalCount){
|
||
showBatchDcvModal(hostnamesWithDns, uuid);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function showBatchDcvModal(hostnamesWithDns, uuid){
|
||
// 按 DNS 域名分组
|
||
var dnsDomains = {};
|
||
hostnamesWithDns.forEach(function(item){
|
||
var dnsDomain = item.dnsDomain;
|
||
if(!dnsDomains[dnsDomain]){
|
||
dnsDomains[dnsDomain] = [];
|
||
}
|
||
dnsDomains[dnsDomain].push(item);
|
||
});
|
||
|
||
var hostnamesHtml = '';
|
||
var domainIndex = 0;
|
||
|
||
// 为每个 DNS 域名创建分组
|
||
for(var dnsDomain in dnsDomains){
|
||
if(dnsDomains.hasOwnProperty(dnsDomain)){
|
||
var domainItems = dnsDomains[dnsDomain];
|
||
var firstItem = domainItems[0];
|
||
var firstHostname = firstItem.hostname;
|
||
var cnameFullName = '_acme-challenge.' + firstHostname;
|
||
var cnameTarget;
|
||
var displayCnameTarget;
|
||
if(domainItems.length > 1){
|
||
// 多个主机名:显示 当前记录.dns域名.UUID.dcv.cloudflare.com
|
||
displayCnameTarget = '当前记录.' + dnsDomain + '.' + uuid + '.dcv.cloudflare.com';
|
||
} else {
|
||
// 单个主机名:保持原格式 主机名.UUID.dcv.cloudflare.com
|
||
displayCnameTarget = firstHostname + '.' + uuid + '.dcv.cloudflare.com';
|
||
}
|
||
// 实际 CNAME 目标格式:主机名.UUID.dcv.cloudflare.com
|
||
cnameTarget = firstHostname + '.' + uuid + '.dcv.cloudflare.com';
|
||
|
||
hostnamesHtml += '<div style="margin-bottom:16px;padding:12px;border:1px solid #e5e5e5;border-radius:4px;">';
|
||
hostnamesHtml += '<div style="font-weight:bold;margin-bottom:8px;">DNS 域名: ' + htmlEscape(dnsDomain) + '</div>';
|
||
|
||
// 显示该 DNS 域名下的所有主机名
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;margin-bottom:8px;">';
|
||
hostnamesHtml += '主机名: ';
|
||
domainItems.forEach(function(item, idx){
|
||
if(idx > 0) hostnamesHtml += ', ';
|
||
hostnamesHtml += htmlEscape(item.hostname);
|
||
});
|
||
hostnamesHtml += '</div>';
|
||
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;margin-bottom:12px;">';
|
||
hostnamesHtml += 'CNAME 目标: <code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(displayCnameTarget) + '</code>';
|
||
hostnamesHtml += '</div>';
|
||
|
||
hostnamesHtml += '<div class="form-group" style="margin-bottom:0;">';
|
||
hostnamesHtml += '<label class="control-label" style="font-size:12px;">选择解析服务商</label>';
|
||
hostnamesHtml += '<select id="dnsProvider_' + domainIndex + '" class="form-control" required>';
|
||
hostnamesHtml += '<option value="">请选择</option>';
|
||
|
||
// 填充 DNS 服务商选项
|
||
if(firstItem.targets.length > 0){
|
||
firstItem.targets.forEach(function(target){
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var displayName = target.domain_name + ' (' + providerName + ')';
|
||
hostnamesHtml += '<option value="' + target.domain_id + '">' + htmlEscape(displayName) + '</option>';
|
||
});
|
||
}
|
||
|
||
hostnamesHtml += '</select>';
|
||
hostnamesHtml += '</div>';
|
||
hostnamesHtml += '</div>';
|
||
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
$('#batchDcvAlert').text('确定要为选中的 ' + hostnamesWithDns.length + ' 个自定义主机名添加 DCV 委派 CNAME 记录吗?');
|
||
$('#batchDcvHostnameList').html(hostnamesHtml);
|
||
$("#modal-batch-dcv").modal('show');
|
||
}
|
||
|
||
function confirmBatchDcv(){
|
||
// 禁用按钮,防止重复点击
|
||
var confirmBtn = $("#modal-batch-dcv .btn-info");
|
||
confirmBtn.prop('disabled', true).text('处理中...');
|
||
|
||
var uuid = $.trim($('#dcvDelegationUuid').val());
|
||
if(!uuid){
|
||
confirmBtn.prop('disabled', false).text('开始批量添加');
|
||
layer.msg('请先获取 DCV 委派 UUID', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 先获取所有主机名的 DNS 信息,按实际 DNS 域名分组
|
||
var hostnamesWithDns = [];
|
||
var processedCount = 0;
|
||
var totalCount = batchDcvHostnamesData.length;
|
||
|
||
// 收集所有主机名的 DNS 信息
|
||
batchDcvHostnamesData.forEach(function(row, index){
|
||
var hostname = row.hostname;
|
||
var cnameFullName = '_acme-challenge.' + hostname;
|
||
|
||
resolveDcvCnameTargets(cnameFullName, function(targets){
|
||
processedCount++;
|
||
|
||
if(targets.length > 0){
|
||
// 使用第一个目标域名作为该主机名的 DNS 域名
|
||
var dnsDomain = targets[0].domain_name;
|
||
hostnamesWithDns.push({
|
||
row: row,
|
||
hostname: hostname,
|
||
dnsDomain: dnsDomain,
|
||
targets: targets
|
||
});
|
||
} else {
|
||
// 无法解析 DNS 域名的主机名
|
||
hostnamesWithDns.push({
|
||
row: row,
|
||
hostname: hostname,
|
||
dnsDomain: '未知域名',
|
||
targets: []
|
||
});
|
||
}
|
||
|
||
if(processedCount === totalCount){
|
||
processBatchDcv(hostnamesWithDns, uuid, confirmBtn);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function processBatchDcv(hostnamesWithDns, uuid, confirmBtn){
|
||
// 按 DNS 域名分组
|
||
var dnsDomains = {};
|
||
hostnamesWithDns.forEach(function(item){
|
||
var dnsDomain = item.dnsDomain;
|
||
if(!dnsDomains[dnsDomain]){
|
||
dnsDomains[dnsDomain] = [];
|
||
}
|
||
dnsDomains[dnsDomain].push(item);
|
||
});
|
||
|
||
// 检查是否所有 DNS 域名都选择了 DNS 服务商
|
||
var allSelected = true;
|
||
var domainIndex = 0;
|
||
for(var dnsDomain in dnsDomains){
|
||
if(dnsDomains.hasOwnProperty(dnsDomain)){
|
||
var selectedDomainId = $('#dnsProvider_' + domainIndex).val();
|
||
if(!selectedDomainId){
|
||
allSelected = false;
|
||
break;
|
||
}
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
if(!allSelected){
|
||
confirmBtn.prop('disabled', false).text('开始批量添加');
|
||
layer.msg('请为所有 DNS 域名选择解析服务商', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
var ii = layer.load(2);
|
||
var batchResults = {
|
||
success: 0,
|
||
failed: 0,
|
||
errors: []
|
||
};
|
||
var processedCount = 0;
|
||
var totalCount = hostnamesWithDns.length;
|
||
|
||
// 按 DNS 域名处理
|
||
var currentDomainIndex = 0;
|
||
for(var dnsDomain in dnsDomains){
|
||
if(dnsDomains.hasOwnProperty(dnsDomain)){
|
||
var domainItems = dnsDomains[dnsDomain];
|
||
var selectedDomainId = $('#dnsProvider_' + currentDomainIndex).val();
|
||
|
||
// 为该 DNS 域名下的所有主机名使用同一个 DNS 服务商(直接用已解析的 targets)
|
||
domainItems.forEach(function(item){
|
||
var hostname = item.hostname;
|
||
var cnameFullName = '_acme-challenge.' + hostname;
|
||
var cnameTarget = hostname + '.' + uuid + '.dcv.cloudflare.com';
|
||
var targets = item.targets;
|
||
|
||
if(!targets.length){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到对应的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
var selectedTarget = null;
|
||
for(var i = 0; i < targets.length; i++){
|
||
if(String(targets[i].domain_id) === String(selectedDomainId)){
|
||
selectedTarget = targets[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!selectedTarget){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到选中的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
submitBatchDcvCnameRecord(cnameFullName, cnameTarget, selectedTarget, function(success, error){
|
||
if(success){
|
||
batchResults.success++;
|
||
} else {
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': ' + error);
|
||
}
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
});
|
||
});
|
||
|
||
currentDomainIndex++;
|
||
}
|
||
}
|
||
|
||
function checkBatchCompletion(){
|
||
if(processedCount === totalCount){
|
||
layer.close(ii);
|
||
// 恢复按钮状态
|
||
confirmBtn.prop('disabled', false).text('开始批量添加');
|
||
|
||
$("#modal-batch-dcv").modal('hide');
|
||
|
||
var message = '批量 DCV 委派完成:\n成功 ' + batchResults.success + ' 个,失败 ' + batchResults.failed + ' 个';
|
||
if(batchResults.errors.length > 0){
|
||
message += '\n\n失败详情:\n' + batchResults.errors.join('\n');
|
||
}
|
||
|
||
layer.alert(message, {icon: batchResults.failed > 0 ? 2 : 1});
|
||
}
|
||
}
|
||
}
|
||
|
||
function submitBatchDcvCnameRecord(cnameFullName, cnameValue, target, callback){
|
||
// 先获取目标域名的默认线路
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/get_domain_default_line',
|
||
data: {domain_id: target.domain_id},
|
||
dataType: 'json',
|
||
success: function(lineRes){
|
||
var line = (lineRes.code === 0 && lineRes.data && lineRes.data.default_line) ? lineRes.data.default_line : '0';
|
||
// 使用获取到的线路添加记录
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/record/add/' + target.domain_id,
|
||
data: {
|
||
name: target.record_name,
|
||
type: 'CNAME',
|
||
value: cnameValue,
|
||
line: line,
|
||
ttl: 600,
|
||
mx: 1,
|
||
weight: 0,
|
||
remark: 'Cloudflare DCV 委派'
|
||
},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
if(res.code === 0){
|
||
callback(true);
|
||
}else{
|
||
callback(false, res.msg);
|
||
}
|
||
},
|
||
error: function(){
|
||
callback(false, '服务器错误');
|
||
}
|
||
});
|
||
},
|
||
error: function(){
|
||
callback(false, '获取默认线路失败');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== 批量主机名 TXT 验证 ====================
|
||
|
||
function batchHostnameTxtVerification(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
if(selectedRows.length === 0){
|
||
layer.msg('请先选择要添加主机名验证的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 过滤出有待验证信息的主机名
|
||
var validHostnames = [];
|
||
var invalidHostnames = [];
|
||
|
||
selectedRows.forEach(function(row){
|
||
var ownership = row.ownership_verification || {};
|
||
if(ownership.name && ownership.value){
|
||
validHostnames.push({
|
||
row: row,
|
||
txtName: ownership.name || '',
|
||
txtValue: ownership.value || ''
|
||
});
|
||
} else {
|
||
invalidHostnames.push(row.hostname);
|
||
}
|
||
});
|
||
|
||
if(validHostnames.length === 0){
|
||
layer.msg('所选主机名都没有获取到主机名验证信息,请先刷新获取', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 显示警告信息
|
||
var warningMsg = '';
|
||
if(invalidHostnames.length > 0){
|
||
warningMsg = '(注意:有 ' + invalidHostnames.length + ' 个主机名无法获取验证信息,已跳过)';
|
||
}
|
||
|
||
// 先获取所有主机名的 DNS 信息,按实际 DNS 域名分组
|
||
var processedCount = 0;
|
||
var totalCount = validHostnames.length;
|
||
var dnsDomains = {};
|
||
|
||
validHostnames.forEach(function(item, idx){
|
||
var txtName = item.txtName;
|
||
var txtValue = item.txtValue;
|
||
|
||
resolveTxtTargets(txtName, txtValue, function(targets){
|
||
processedCount++;
|
||
|
||
// 获取该主机名的 DNS 域名
|
||
var dnsDomain = targets.length > 0 ? targets[0].domain_name : '未知域名';
|
||
|
||
if(!dnsDomains[dnsDomain]){
|
||
dnsDomains[dnsDomain] = {
|
||
items: [],
|
||
targets: targets.length > 0 ? targets : []
|
||
};
|
||
}
|
||
dnsDomains[dnsDomain].items.push(item);
|
||
|
||
if(processedCount === totalCount){
|
||
showBatchHostnameTxtModal(dnsDomains, validHostnames.length, invalidHostnames, warningMsg);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function showBatchHostnameTxtModal(dnsDomains, totalCount, invalidHostnames, warningMsg){
|
||
var hostnamesHtml = '';
|
||
var domainIndex = 0;
|
||
batchHostnameTxtData = dnsDomains;
|
||
|
||
// 为每个 DNS 域名创建分组
|
||
for(var dnsDomain in dnsDomains){
|
||
if(dnsDomains.hasOwnProperty(dnsDomain)){
|
||
var domainData = dnsDomains[dnsDomain];
|
||
var domainItems = domainData.items;
|
||
var targets = domainData.targets;
|
||
|
||
hostnamesHtml += '<div style="margin-bottom:16px;padding:12px;border:1px solid #e5e5e5;border-radius:4px;">';
|
||
hostnamesHtml += '<div style="font-weight:bold;margin-bottom:8px;">DNS 域名: ' + htmlEscape(dnsDomain) + '</div>';
|
||
|
||
// 为每个主机名显示详细信息
|
||
domainItems.forEach(function(item, idx){
|
||
hostnamesHtml += '<div style="margin-top:8px;padding:8px;background:#f9f9f9;border-radius:4px;">';
|
||
hostnamesHtml += '<div style="font-weight:bold;font-size:12px;margin-bottom:4px;">主机名: ' + htmlEscape(item.row.hostname) + '</div>';
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;">';
|
||
hostnamesHtml += 'TXT 名称: <code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(item.txtName) + '</code><br>';
|
||
hostnamesHtml += 'TXT 值: <code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(item.txtValue) + '</code>';
|
||
hostnamesHtml += '</div>';
|
||
hostnamesHtml += '</div>';
|
||
});
|
||
|
||
hostnamesHtml += '<div class="form-group" style="margin-bottom:0;margin-top:12px;">';
|
||
hostnamesHtml += '<label class="control-label" style="font-size:12px;">选择解析服务商</label>';
|
||
hostnamesHtml += '<select id="txtDnsProvider_' + domainIndex + '" class="form-control" required>';
|
||
hostnamesHtml += '<option value="">请选择</option>';
|
||
|
||
// 填充 DNS 服务商选项
|
||
targets.forEach(function(target){
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var displayName = target.domain_name + ' (' + providerName + ')';
|
||
hostnamesHtml += '<option value="' + target.domain_id + '">' + htmlEscape(displayName) + '</option>';
|
||
});
|
||
|
||
hostnamesHtml += '</select>';
|
||
hostnamesHtml += '</div>';
|
||
hostnamesHtml += '</div>';
|
||
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
var alertMsg = '确定要为 ' + totalCount + ' 个自定义主机名添加主机名 TXT 验证记录吗?' + warningMsg;
|
||
if(invalidHostnames.length > 0){
|
||
alertMsg += '<br>无法获取验证信息的主机名:' + invalidHostnames.join(', ');
|
||
}
|
||
$('#batchHostnameTxtAlert').html(alertMsg);
|
||
$('#batchHostnameTxtList').html(hostnamesHtml);
|
||
$("#modal-batch-hostname-txt").modal('show');
|
||
}
|
||
|
||
var batchHostnameTxtData = [];
|
||
|
||
function confirmBatchHostnameTxt(){
|
||
// 检查是否所有 DNS 域名都选择了 DNS 服务商
|
||
var allSelected = true;
|
||
var domainIndex = 0;
|
||
for(var dnsDomain in batchHostnameTxtData){
|
||
if(batchHostnameTxtData.hasOwnProperty(dnsDomain)){
|
||
var selectedDomainId = $('#txtDnsProvider_' + domainIndex).val();
|
||
if(!selectedDomainId){
|
||
allSelected = false;
|
||
break;
|
||
}
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
if(!allSelected){
|
||
layer.msg('请为所有 DNS 域名选择解析服务商', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 禁用按钮,防止重复点击
|
||
var confirmBtn = $("#modal-batch-hostname-txt .btn-success");
|
||
confirmBtn.prop('disabled', true).text('处理中...');
|
||
|
||
var ii = layer.load(2);
|
||
var batchResults = {
|
||
success: 0,
|
||
failed: 0,
|
||
errors: []
|
||
};
|
||
var processedCount = 0;
|
||
var totalCount = 0;
|
||
|
||
// 计算总主机名数量
|
||
for(var dnsDomain in batchHostnameTxtData){
|
||
if(batchHostnameTxtData.hasOwnProperty(dnsDomain)){
|
||
totalCount += batchHostnameTxtData[dnsDomain].items.length;
|
||
}
|
||
}
|
||
|
||
// 按 DNS 域名处理
|
||
var currentDomainIndex = 0;
|
||
for(var dnsDomain in batchHostnameTxtData){
|
||
if(batchHostnameTxtData.hasOwnProperty(dnsDomain)){
|
||
var domainData = batchHostnameTxtData[dnsDomain];
|
||
var domainItems = domainData.items;
|
||
var targets = domainData.targets;
|
||
var selectedDomainId = $('#txtDnsProvider_' + currentDomainIndex).val();
|
||
|
||
// 查找用户选择的目标
|
||
var selectedTarget = null;
|
||
for(var i = 0; i < targets.length; i++){
|
||
if(String(targets[i].domain_id) === String(selectedDomainId)){
|
||
selectedTarget = targets[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 为该 DNS 域名下的所有主机名添加 TXT 记录(每个主机名单独解析以获取正确的 record_name)
|
||
domainItems.forEach(function(item){
|
||
var hostname = item.row.hostname;
|
||
var txtName = item.txtName;
|
||
var txtValue = item.txtValue;
|
||
|
||
if(!selectedTarget){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到选中的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
// 每个主机名的 TXT 名称不同,需要单独解析获取正确的 record_name
|
||
resolveTxtTargets(txtName, txtValue, function(targetsForHostname){
|
||
if(!targetsForHostname.length){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到对应的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
var targetForThisHostname = null;
|
||
for(var j = 0; j < targetsForHostname.length; j++){
|
||
if(String(targetsForHostname[j].domain_id) === String(selectedDomainId)){
|
||
targetForThisHostname = targetsForHostname[j];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!targetForThisHostname){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到选中的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
submitBatchTxtRecord(txtName, txtValue, targetForThisHostname, 'Cloudflare 主机名验证', function(success, error){
|
||
processedCount++;
|
||
if(success){
|
||
batchResults.success++;
|
||
} else {
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': ' + error);
|
||
}
|
||
checkBatchCompletion();
|
||
});
|
||
});
|
||
});
|
||
|
||
currentDomainIndex++;
|
||
}
|
||
}
|
||
|
||
function checkBatchCompletion(){
|
||
if(processedCount === totalCount){
|
||
layer.close(ii);
|
||
// 恢复按钮状态
|
||
var confirmBtn = $("#modal-batch-hostname-txt .btn-success");
|
||
confirmBtn.prop('disabled', false).text('开始添加');
|
||
|
||
$("#modal-batch-hostname-txt").modal('hide');
|
||
|
||
var message = '批量主机名 TXT 验证完成:\n成功 ' + batchResults.success + ' 个,失败 ' + batchResults.failed + ' 个';
|
||
if(batchResults.errors.length > 0){
|
||
message += '\n\n失败详情:\n' + batchResults.errors.join('\n');
|
||
}
|
||
|
||
layer.alert(message, {icon: batchResults.failed > 0 ? 2 : 1});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 批量证书 TXT 验证 ====================
|
||
|
||
function batchCertTxtVerification(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
if(selectedRows.length === 0){
|
||
layer.msg('请先选择要添加证书验证的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 过滤出有待验证信息的主机名
|
||
var validHostnames = [];
|
||
var invalidHostnames = [];
|
||
|
||
selectedRows.forEach(function(row){
|
||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||
if(records.length > 0 && records[0].txt_name && records[0].txt_value){
|
||
validHostnames.push({
|
||
row: row,
|
||
txtName: records[0].txt_name || '',
|
||
txtValue: records[0].txt_value || ''
|
||
});
|
||
} else {
|
||
invalidHostnames.push(row.hostname);
|
||
}
|
||
});
|
||
|
||
if(validHostnames.length === 0){
|
||
layer.msg('所选主机名都没有获取到证书验证信息,请先刷新获取', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 显示警告信息
|
||
var warningMsg = '';
|
||
if(invalidHostnames.length > 0){
|
||
warningMsg = '(注意:有 ' + invalidHostnames.length + ' 个主机名无法获取验证信息,已跳过)';
|
||
}
|
||
|
||
// 先获取所有主机名的 DNS 信息,按实际 DNS 域名分组
|
||
var processedCount = 0;
|
||
var totalCount = validHostnames.length;
|
||
var dnsDomains = {};
|
||
|
||
validHostnames.forEach(function(item, idx){
|
||
var txtName = item.txtName;
|
||
var txtValue = item.txtValue;
|
||
|
||
resolveTxtTargets(txtName, txtValue, function(targets){
|
||
processedCount++;
|
||
|
||
// 获取该主机名的 DNS 域名
|
||
var dnsDomain = targets.length > 0 ? targets[0].domain_name : '未知域名';
|
||
|
||
if(!dnsDomains[dnsDomain]){
|
||
dnsDomains[dnsDomain] = {
|
||
items: [],
|
||
targets: targets.length > 0 ? targets : []
|
||
};
|
||
}
|
||
dnsDomains[dnsDomain].items.push(item);
|
||
|
||
if(processedCount === totalCount){
|
||
showBatchCertTxtModal(dnsDomains, validHostnames.length, invalidHostnames, warningMsg);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function showBatchCertTxtModal(dnsDomains, totalCount, invalidHostnames, warningMsg){
|
||
var hostnamesHtml = '';
|
||
var domainIndex = 0;
|
||
batchCertTxtData = dnsDomains;
|
||
|
||
// 为每个 DNS 域名创建分组
|
||
for(var dnsDomain in dnsDomains){
|
||
if(dnsDomains.hasOwnProperty(dnsDomain)){
|
||
var domainData = dnsDomains[dnsDomain];
|
||
var domainItems = domainData.items;
|
||
var targets = domainData.targets;
|
||
|
||
hostnamesHtml += '<div style="margin-bottom:16px;padding:12px;border:1px solid #e5e5e5;border-radius:4px;">';
|
||
hostnamesHtml += '<div style="font-weight:bold;margin-bottom:8px;">DNS 域名: ' + htmlEscape(dnsDomain) + '</div>';
|
||
|
||
// 为每个主机名显示详细信息
|
||
domainItems.forEach(function(item, idx){
|
||
hostnamesHtml += '<div style="margin-top:8px;padding:8px;background:#f9f9f9;border-radius:4px;">';
|
||
hostnamesHtml += '<div style="font-weight:bold;font-size:12px;margin-bottom:4px;">主机名: ' + htmlEscape(item.row.hostname) + '</div>';
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;">';
|
||
hostnamesHtml += 'TXT 名称: <code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(item.txtName) + '</code><br>';
|
||
hostnamesHtml += 'TXT 值: <code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(item.txtValue) + '</code>';
|
||
hostnamesHtml += '</div>';
|
||
hostnamesHtml += '</div>';
|
||
});
|
||
|
||
hostnamesHtml += '<div class="form-group" style="margin-bottom:0;margin-top:12px;">';
|
||
hostnamesHtml += '<label class="control-label" style="font-size:12px;">选择解析服务商</label>';
|
||
hostnamesHtml += '<select id="certDnsProvider_' + domainIndex + '" class="form-control" required>';
|
||
hostnamesHtml += '<option value="">请选择</option>';
|
||
|
||
// 填充 DNS 服务商选项
|
||
targets.forEach(function(target){
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var displayName = target.domain_name + ' (' + providerName + ')';
|
||
hostnamesHtml += '<option value="' + target.domain_id + '">' + htmlEscape(displayName) + '</option>';
|
||
});
|
||
|
||
hostnamesHtml += '</select>';
|
||
hostnamesHtml += '</div>';
|
||
hostnamesHtml += '</div>';
|
||
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
var alertMsg = '确定要为 ' + totalCount + ' 个自定义主机名添加证书 TXT 验证记录吗?' + warningMsg;
|
||
if(invalidHostnames.length > 0){
|
||
alertMsg += '<br>无法获取验证信息的主机名:' + invalidHostnames.join(', ');
|
||
}
|
||
$('#batchCertTxtAlert').html(alertMsg);
|
||
$('#batchCertTxtList').html(hostnamesHtml);
|
||
$("#modal-batch-cert-txt").modal('show');
|
||
}
|
||
|
||
var batchCertTxtData = [];
|
||
|
||
function confirmBatchCertTxt(){
|
||
// 检查是否所有 DNS 域名都选择了 DNS 服务商
|
||
var allSelected = true;
|
||
var domainIndex = 0;
|
||
for(var dnsDomain in batchCertTxtData){
|
||
if(batchCertTxtData.hasOwnProperty(dnsDomain)){
|
||
var selectedDomainId = $('#certDnsProvider_' + domainIndex).val();
|
||
if(!selectedDomainId){
|
||
allSelected = false;
|
||
break;
|
||
}
|
||
domainIndex++;
|
||
}
|
||
}
|
||
|
||
if(!allSelected){
|
||
layer.msg('请为所有 DNS 域名选择解析服务商', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 禁用按钮,防止重复点击
|
||
var confirmBtn = $("#modal-batch-cert-txt .btn-success");
|
||
confirmBtn.prop('disabled', true).text('处理中...');
|
||
|
||
var ii = layer.load(2);
|
||
var batchResults = {
|
||
success: 0,
|
||
failed: 0,
|
||
errors: []
|
||
};
|
||
var processedCount = 0;
|
||
var totalCount = 0;
|
||
|
||
// 计算总主机名数量
|
||
for(var dnsDomain in batchCertTxtData){
|
||
if(batchCertTxtData.hasOwnProperty(dnsDomain)){
|
||
totalCount += batchCertTxtData[dnsDomain].items.length;
|
||
}
|
||
}
|
||
|
||
// 按 DNS 域名处理
|
||
var currentDomainIndex = 0;
|
||
for(var dnsDomain in batchCertTxtData){
|
||
if(batchCertTxtData.hasOwnProperty(dnsDomain)){
|
||
var domainData = batchCertTxtData[dnsDomain];
|
||
var domainItems = domainData.items;
|
||
var targets = domainData.targets;
|
||
var selectedDomainId = $('#certDnsProvider_' + currentDomainIndex).val();
|
||
|
||
// 查找用户选择的目标
|
||
var selectedTarget = null;
|
||
for(var i = 0; i < targets.length; i++){
|
||
if(String(targets[i].domain_id) === String(selectedDomainId)){
|
||
selectedTarget = targets[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 为该 DNS 域名下的所有主机名添加 TXT 记录(每个主机名单独解析以获取正确的 record_name)
|
||
domainItems.forEach(function(item){
|
||
var hostname = item.row.hostname;
|
||
var txtName = item.txtName;
|
||
var txtValue = item.txtValue;
|
||
|
||
if(!selectedTarget){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到选中的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
// 每个主机名的 TXT 名称不同,需要单独解析获取正确的 record_name
|
||
resolveTxtTargets(txtName, txtValue, function(targetsForHostname){
|
||
if(!targetsForHostname.length){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到对应的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
var targetForThisHostname = null;
|
||
for(var j = 0; j < targetsForHostname.length; j++){
|
||
if(String(targetsForHostname[j].domain_id) === String(selectedDomainId)){
|
||
targetForThisHostname = targetsForHostname[j];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!targetForThisHostname){
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': 未找到选中的解析域名');
|
||
processedCount++;
|
||
checkBatchCompletion();
|
||
return;
|
||
}
|
||
|
||
submitBatchTxtRecord(txtName, txtValue, targetForThisHostname, 'Cloudflare 证书验证', function(success, error){
|
||
processedCount++;
|
||
if(success){
|
||
batchResults.success++;
|
||
} else {
|
||
batchResults.failed++;
|
||
batchResults.errors.push(hostname + ': ' + error);
|
||
}
|
||
checkBatchCompletion();
|
||
});
|
||
});
|
||
});
|
||
|
||
currentDomainIndex++;
|
||
}
|
||
}
|
||
|
||
function checkBatchCompletion(){
|
||
if(processedCount === totalCount){
|
||
layer.close(ii);
|
||
// 恢复按钮状态
|
||
var confirmBtn = $("#modal-batch-cert-txt .btn-success");
|
||
confirmBtn.prop('disabled', false).text('开始添加');
|
||
|
||
$("#modal-batch-cert-txt").modal('hide');
|
||
|
||
var message = '批量证书 TXT 验证完成:\n成功 ' + batchResults.success + ' 个,失败 ' + batchResults.failed + ' 个';
|
||
if(batchResults.errors.length > 0){
|
||
message += '\n\n失败详情:\n' + batchResults.errors.join('\n');
|
||
}
|
||
|
||
layer.alert(message, {icon: batchResults.failed > 0 ? 2 : 1});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量刷新验证
|
||
function batchRefreshVerification(){
|
||
var selections = $('#listTable').bootstrapTable('getSelections');
|
||
if(!selections || selections.length === 0){
|
||
layer.msg('请先选择要刷新验证的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 显示选中的主机名列表
|
||
var hostnamesHtml = '';
|
||
selections.forEach(function(row, index){
|
||
hostnamesHtml += '<div style="padding:8px;border-bottom:1px solid #eee;' + (index === 0 ? 'border-top:1px solid #eee;' : '') + '">';
|
||
hostnamesHtml += '<div style="font-weight:bold;">' + htmlEscape(row.hostname) + '</div>';
|
||
if(row.ssl_status){
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;">SSL 状态: ' + formatStatusText(row.ssl_status) + '</div>';
|
||
}
|
||
if(row.verification_status && row.verification_status !== '-'){
|
||
hostnamesHtml += '<div style="font-size:12px;color:#666;">主机名验证: ' + formatStatusText(row.verification_status) + '</div>';
|
||
}
|
||
hostnamesHtml += '</div>';
|
||
});
|
||
|
||
$('#batchRefreshAlert').html('确定要为选中的 <strong>' + selections.length + '</strong> 个自定义主机名重新发起验证吗?');
|
||
$('#batchRefreshList').html(hostnamesHtml);
|
||
$("#modal-batch-refresh").modal('show');
|
||
}
|
||
|
||
function confirmBatchRefresh(){
|
||
var selections = $('#listTable').bootstrapTable('getSelections');
|
||
if(!selections || selections.length === 0){
|
||
layer.msg('请先选择要刷新验证的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 禁用按钮
|
||
var confirmBtn = $("#modal-batch-refresh .btn-success");
|
||
confirmBtn.prop('disabled', true).text('处理中...');
|
||
|
||
var ii = layer.load(2);
|
||
var batchResults = {
|
||
success: 0,
|
||
failed: 0,
|
||
errors: []
|
||
};
|
||
|
||
var totalCount = selections.length;
|
||
var processedCount = 0;
|
||
|
||
function checkBatchCompletion(){
|
||
if(processedCount === totalCount){
|
||
layer.close(ii);
|
||
// 恢复按钮状态
|
||
confirmBtn.prop('disabled', false).text('开始刷新');
|
||
|
||
$("#modal-batch-refresh").modal('hide');
|
||
|
||
var message = '批量刷新验证完成:\n成功 ' + batchResults.success + ' 个,失败 ' + batchResults.failed + ' 个';
|
||
if(batchResults.errors.length > 0){
|
||
message += '\n\n失败详情:\n' + batchResults.errors.join('\n');
|
||
}
|
||
|
||
layer.alert(message, {icon: batchResults.failed > 0 ? 2 : 1});
|
||
|
||
// 刷新主机名列表以显示最新状态
|
||
refreshHostnameList();
|
||
}
|
||
}
|
||
|
||
// 为每个选中的主机名发起刷新验证请求
|
||
selections.forEach(function(row){
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/refresh/{$domainId}',
|
||
data: {hostname_id: row.id},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
processedCount++;
|
||
if(res.code === 0){
|
||
batchResults.success++;
|
||
} else {
|
||
batchResults.failed++;
|
||
batchResults.errors.push(row.hostname + ': ' + (res.msg || '未知错误'));
|
||
}
|
||
checkBatchCompletion();
|
||
},
|
||
error: function(){
|
||
processedCount++;
|
||
batchResults.failed++;
|
||
batchResults.errors.push(row.hostname + ': 网络错误或服务器无响应');
|
||
checkBatchCompletion();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==================== 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 和 label(label 用于兜底匹配)
|
||
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({
|
||
type: 'POST',
|
||
url: '/cloudflare/get_domain_default_line',
|
||
data: {domain_id: target.domain_id},
|
||
dataType: 'json',
|
||
success: function(lineRes){
|
||
var line = (lineRes.code === 0 && lineRes.data && lineRes.data.default_line) ? lineRes.data.default_line : '0';
|
||
// 使用获取到的线路添加记录
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/record/add/' + target.domain_id,
|
||
data: {
|
||
name: target.record_name,
|
||
type: 'TXT',
|
||
value: txtValue,
|
||
line: line,
|
||
ttl: 600,
|
||
mx: 1,
|
||
weight: 0,
|
||
remark: remark
|
||
},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
if(res.code === 0){
|
||
callback(true);
|
||
}else{
|
||
callback(false, res.msg);
|
||
}
|
||
},
|
||
error: function(){
|
||
callback(false, '服务器错误');
|
||
}
|
||
});
|
||
},
|
||
error: function(){
|
||
callback(false, '获取默认线路失败');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 解析 TXT 记录的目标域名
|
||
function resolveTxtTargets(txtName, txtValue, callback){
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/txttargets/' + domainId,
|
||
data: {hostname: txtName},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
if(res.code === 0 && res.data && res.data.candidates){
|
||
callback(res.data.candidates);
|
||
} else {
|
||
callback([]);
|
||
}
|
||
},
|
||
error: function(){
|
||
callback([]);
|
||
}
|
||
});
|
||
}
|
||
|
||
function batchEditHostnames(){
|
||
var selectedRows = $("#listTable").bootstrapTable('getSelections');
|
||
if(selectedRows.length === 0){
|
||
layer.msg('请先选择要修改的自定义主机名', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 设置批量修改对话框的内容
|
||
$('#batchEditAlert').text('批量修改 ' + selectedRows.length + ' 个自定义主机名的设置');
|
||
$('#batchEditForm input[name=hostname_ids]').val(selectedRows.map(function(row){ return row.id; }).join(','));
|
||
$('#batchEditForm input[name=custom_origin_server]').val('');
|
||
$('#batchEditForm select[name=ssl_method]').val('');
|
||
$('#batchEditForm select[name=min_tls_version]').val('');
|
||
|
||
$("#modal-batch-edit").modal('show');
|
||
}
|
||
|
||
function submitBatchEdit(){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/batch_update/{$domainId}',
|
||
data: $("#batchEditForm").serialize(),
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#modal-batch-edit").modal('hide');
|
||
layer.msg('批量修改成功', {icon: 1, time: 1500});
|
||
refreshHostnameList();
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function batchAddHostnames(){
|
||
// 重置批量添加对话框
|
||
$('#batchAddForm')[0].reset();
|
||
|
||
$("#modal-batch-add").modal('show');
|
||
}
|
||
|
||
function submitBatchAdd(){
|
||
var hostnamesText = $("#batchAddForm textarea[name=hostnames]").val();
|
||
if(!hostnamesText.trim()){
|
||
layer.msg('请输入主机名列表', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/batch_add/{$domainId}',
|
||
data: $("#batchAddForm").serialize(),
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#modal-batch-add").modal('hide');
|
||
layer.msg('批量添加成功', {icon: 1, time: 1500});
|
||
refreshHostnameList();
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadDcvDelegationUuid(){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/dcv_delegation_uuid/{$domainId}',
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
var uuid = res.data && res.data.uuid ? res.data.uuid : '';
|
||
if(uuid){
|
||
$("#dcvDelegationUuid").val(uuid);
|
||
$('#btnQuickAddDcv').prop('disabled', false).attr('title', '点击一键添加 DCV 委派 CNAME 记录');
|
||
layer.msg('获取 DCV 委派 UUID 成功', {icon: 1, time: 1000});
|
||
} else {
|
||
$("#dcvDelegationUuid").val('');
|
||
$('#btnQuickAddDcv').prop('disabled', true).attr('title', '需要先获取到 UUID');
|
||
layer.msg('未获取到 DCV 委派 UUID', {icon: 0, time: 1000});
|
||
}
|
||
} else {
|
||
$("#dcvDelegationUuid").val('');
|
||
$('#btnQuickAddDcv').prop('disabled', true).attr('title', '需要先获取到 UUID');
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
$("#dcvDelegationUuid").val('');
|
||
$('#btnQuickAddDcv').prop('disabled', true).attr('title', '需要先获取到 UUID');
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function showDcvDelegationHelp(){
|
||
var fullCnameTarget = $.trim($('#dcvDelegationUuid').val());
|
||
var uuid = '';
|
||
|
||
// 从完整的 CNAME 目标中提取 UUID
|
||
if(fullCnameTarget){
|
||
var match = fullCnameTarget.match(/\.([a-f0-9-]+)\.dcv\.cloudflare\.com$/i);
|
||
if(match && match[1]){
|
||
uuid = match[1];
|
||
} else {
|
||
// 尝试直接使用输入值作为 UUID
|
||
uuid = fullCnameTarget;
|
||
}
|
||
}
|
||
|
||
var html = '';
|
||
html += '<div class="alert alert-info">';
|
||
html += '<strong>什么是 DCV 委派?</strong><br>';
|
||
html += 'DCV 委派用于为未代理或通配符主机名启用自动证书颁发和续订。对于每个主机名,域所有者需要使用权威 DNS 放置一个 CNAME 记录,将 ACME DCV 质询指向主机名特定的 Cloudflare 验证目标。';
|
||
html += '</div>';
|
||
html += '<div class="alert alert-success">';
|
||
html += '<strong>如何使用 DCV 委派?</strong><br>';
|
||
html += '如果您想要获得 example.com 和 *.example.com(通配符自定义主机名)证书,而无需在每个续订期间手动放置 TXT 令牌:<br>';
|
||
html += '<ul>';
|
||
html += '<li>在权威 DNS 中在 <code style="word-wrap:break-word;word-break:break-all;">_acme-challenge.example.com</code> 上创建 CNAME 记录,并将其指向 <code style="word-wrap:break-word;word-break:break-all;">example.com.' + uuid + '.dcv.cloudflare.com</code></li>';
|
||
html += '<li>一个这样的记录可以同时处理顶级主机名和通配符。</li>';
|
||
html += '</ul>';
|
||
html += '</div>';
|
||
html += '<div class="alert alert-danger">';
|
||
html += '<strong>注意事项:</strong><br>';
|
||
html += '<ul>';
|
||
html += '<li>CNAME 目标的 UUID 部分对于您的区域和帐户是唯一的。</li>';
|
||
html += '<li>将域移动到其他帐户将更改 UUID 的值。</li>';
|
||
html += '</ul>';
|
||
html += '</div>';
|
||
|
||
$('#dcvHelpContent').html(html);
|
||
$("#modal-dcv-help").modal('show');
|
||
}
|
||
|
||
function quickAddDcvDelegation(){
|
||
var fullCnameTarget = $.trim($('#dcvDelegationUuid').val());
|
||
if(!fullCnameTarget){
|
||
layer.msg('请先获取 DCV 委派 UUID', {icon: 0});
|
||
return;
|
||
}
|
||
|
||
// 从完整的 CNAME 目标中提取 UUID
|
||
var uuid = '';
|
||
var match = fullCnameTarget.match(/\.([a-f0-9-]+)\.dcv\.cloudflare\.com$/i);
|
||
if(match && match[1]){
|
||
uuid = match[1];
|
||
} else {
|
||
// 尝试直接使用输入值作为 UUID
|
||
uuid = fullCnameTarget;
|
||
}
|
||
|
||
if(!uuid){
|
||
layer.msg('无法从 DCV 委派值中提取 UUID', {icon: 2});
|
||
return;
|
||
}
|
||
|
||
// 获取自定义主机名列表(使用完整原始数据,而不是表格中的已过滤数据)
|
||
var hostnames = allHostnameData.length > 0 ? allHostnameData : $('#listTable').bootstrapTable('getData');
|
||
var hostnameOptions = [];
|
||
|
||
// 添加自定义主机名选项
|
||
for(var i = 0; i < hostnames.length; i++){
|
||
var row = hostnames[i];
|
||
if(row && row.hostname){
|
||
var hostname = row.hostname.toLowerCase();
|
||
hostnameOptions.push({
|
||
id: 'custom_' + i,
|
||
name: '自定义主机名: ' + hostname,
|
||
value: hostname
|
||
});
|
||
}
|
||
}
|
||
|
||
if(hostnameOptions.length === 0){
|
||
layer.alert('未找到可用的自定义主机名', {icon: 2});
|
||
return;
|
||
}
|
||
|
||
// 弹出选择对话框
|
||
openHostnameSelector(hostnameOptions, uuid);
|
||
}
|
||
|
||
function filterDcvHostnamesModal(searchText){
|
||
var searchTextLower = (searchText || '').toLowerCase();
|
||
$('.dcv-hostname-item').each(function(){
|
||
var itemText = $(this).attr('data-search-text') || '';
|
||
if(searchTextLower === '' || itemText.indexOf(searchTextLower) !== -1){
|
||
$(this).show();
|
||
} else {
|
||
$(this).hide();
|
||
}
|
||
});
|
||
}
|
||
|
||
function confirmDcvHostnameSelection(){
|
||
var selectedValue = $('#hostnameSelectorForm input[name=hostnameOption]:checked').val();
|
||
if(!selectedValue){
|
||
layer.msg('请选择一个自定义主机名', {icon: 2});
|
||
return;
|
||
}
|
||
$("#modal-dcv-selector").modal('hide');
|
||
|
||
// 构造 CNAME 记录信息
|
||
var selectedDomain = selectedValue.toLowerCase();
|
||
var cnameFullName = '_acme-challenge.' + selectedDomain;
|
||
var cnameTarget = selectedDomain + '.' + currentDcvUuid + '.dcv.cloudflare.com';
|
||
|
||
// 继续原有逻辑
|
||
resolveDcvCnameTargets(cnameFullName, function(targets){
|
||
if(!targets.length){
|
||
layer.alert('系统中未找到与该域名对应的托管域名,请手动到解析页添加 CNAME 记录<br><br>'
|
||
+ '记录类型:CNAME<br>主机记录:<code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(cnameFullName) + '</code><br>记录值:<code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(cnameTarget) + '</code>', {icon: 2, maxWidth: 350});
|
||
return;
|
||
}
|
||
if(targets.length === 1){
|
||
confirmQuickAddDcvCnameRecord(cnameFullName, cnameTarget, targets[0]);
|
||
return;
|
||
}
|
||
openDcvTargetPicker(cnameFullName, cnameTarget, targets);
|
||
});
|
||
}
|
||
|
||
var currentDcvUuid = '';
|
||
|
||
function openHostnameSelector(options, uuid){
|
||
currentDcvUuid = uuid;
|
||
var html = '';
|
||
for(var i = 0; i < options.length; i++){
|
||
var opt = options[i] || {};
|
||
html += '<div class="radio dcv-hostname-item" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:12px;" data-search-text="' + htmlEscape(opt.value || '').toLowerCase() + '">';
|
||
html += '<label style="display:block;padding-left:22px;word-wrap:break-word;word-break:break-all;font-size:14px;">';
|
||
html += '<input type="radio" name="hostnameOption" value="' + htmlEscape(opt.value || '') + '"' + (i === 0 ? ' checked' : '') + '>';
|
||
html += '<strong style="display:inline-block;max-width:100%;word-wrap:break-word;word-break:break-all;font-size:14px;">' + htmlEscape(opt.name || '') + '</strong>';
|
||
html += '</label>';
|
||
html += '</div>';
|
||
}
|
||
if(options.length === 0){
|
||
html += '<div class="alert alert-warning">未找到匹配的自定义主机名</div>';
|
||
}
|
||
|
||
$('#hostnameSelectorForm').html(html);
|
||
$('#dcvHostnameSearch').val('');
|
||
$("#modal-dcv-selector").modal('show');
|
||
}
|
||
|
||
function resolveDcvCnameTargets(fullName, callback){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/txttargets/{$domainId}',
|
||
data: {hostname: fullName},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
var targets = res.data && $.isArray(res.data.candidates) ? res.data.candidates : [];
|
||
callback(targets);
|
||
}else{
|
||
// 错误时返回空数组,避免流程阻塞
|
||
callback([]);
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
// 错误时返回空数组,避免流程阻塞
|
||
callback([]);
|
||
}
|
||
});
|
||
}
|
||
|
||
var currentDcvTargets = [];
|
||
var currentDcvCnameValue = '';
|
||
|
||
function openDcvTargetPicker(fullName, cnameValue, targets){
|
||
currentDcvTargets = targets;
|
||
currentDcvCnameValue = cnameValue;
|
||
|
||
var html = '';
|
||
for(var i = 0; i < targets.length; i++){
|
||
var target = targets[i] || {};
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||
html += '<div class="radio" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:10px 12px;">';
|
||
html += '<label style="display:block;padding-left:22px;word-wrap:break-word;word-break:break-all;">';
|
||
html += '<input type="radio" name="dcvTarget" value="' + htmlEscape(String(target.domain_id || '')) + '"' + (i === 0 ? ' checked' : '') + '>';
|
||
html += '<strong style="display:inline-block;max-width:100%;word-wrap:break-word;word-break:break-all;">' + htmlEscape(target.domain_name || '-') + '</strong>';
|
||
if(target.is_current_domain){
|
||
html += ' <span class="label label-primary">当前页</span>';
|
||
}
|
||
html += '<div class="help-block" style="margin:8px 0 0;word-wrap:break-word;word-break:break-all;">';
|
||
html += '主机记录:<code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(target.record_name || '@') + '</code><br>';
|
||
html += '服务商:' + htmlEscape(providerName) + '<br>';
|
||
html += '账户:<span style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(accountName) + '</span>';
|
||
html += '</div>';
|
||
html += '</label></div>';
|
||
}
|
||
|
||
$('#dcvTargetHostname').html('<code style="word-wrap:break-word;word-break:break-all;display:block;">' + htmlEscape(fullName) + '</code>');
|
||
$('#dcvTargetValue').val(cnameValue);
|
||
$('#dcvTargetPickerList').html('<form id="dcvTargetPickerForm">' + html + '</form>');
|
||
$("#modal-dcv-target-picker").modal('show');
|
||
}
|
||
|
||
function confirmDcvTargetSelection(){
|
||
var selectedId = $('#dcvTargetPickerForm input[name=dcvTarget]:checked').val();
|
||
var target = findTxtTargetByDomainId(currentDcvTargets, selectedId);
|
||
if(!target){
|
||
layer.msg('请选择要写入的解析域名', {icon: 2});
|
||
return;
|
||
}
|
||
$("#modal-dcv-target-picker").modal('hide');
|
||
submitQuickAddDcvCnameRecord(currentDcvCnameValue, target);
|
||
}
|
||
|
||
function confirmQuickAddDcvCnameRecord(fullName, cnameValue, target){
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||
layer.confirm(
|
||
'<div style="max-width:100%;overflow:hidden;">'
|
||
+ '<p style="margin-bottom:10px;">确定要一键添加 DCV 委派 CNAME 记录吗?</p>'
|
||
+ '<p style="margin-bottom:10px;">此操作将在 <b>' + htmlEscape(target.domain_name || '-') + '</b> 上创建以下记录:</p>'
|
||
+ '<div style="background:#f5f5f5;padding:10px;border-radius:4px;border:1px solid #e5e5e5;font-size:13px;">'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">记录类型</span><span style="color:#d9534f;font-weight:bold;">CNAME</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">主机记录</span><span style="word-wrap:break-word;word-break:break-all;display:inline-block;vertical-align:top;max-width:calc(100% - 80px);color:#337ab7;font-family:Consolas,Monaco,Courier New,monospace;background:#fff;padding:2px 6px;border-radius:3px;border:1px solid #ddd;">' + htmlEscape(fullName) + '</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">记录值</span><span style="word-wrap:break-word;word-break:break-all;display:inline-block;vertical-align:top;max-width:calc(100% - 80px);color:#337ab7;font-family:Consolas,Monaco,Courier New,monospace;background:#fff;padding:2px 6px;border-radius:3px;border:1px solid #ddd;">' + htmlEscape(cnameValue) + '</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">解析域名</span><span>' + htmlEscape(target.domain_name || '-') + '</span></div>'
|
||
+ '<div style="margin-bottom:0;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">服务商</span><span>' + htmlEscape(providerName) + '</span></div>'
|
||
+ '<div style="margin-top:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">账户</span><span>' + htmlEscape(accountName) + '</span></div>'
|
||
+ '</div>'
|
||
+ '</div>',
|
||
{title: '一键添加 DCV 委派', icon: 0},
|
||
function(index){
|
||
layer.close(index);
|
||
submitQuickAddDcvCnameRecord(cnameValue, target);
|
||
}
|
||
);
|
||
}
|
||
|
||
function submitQuickAddDcvCnameRecord(cnameValue, target){
|
||
var ii = layer.load(2);
|
||
// 先获取目标域名的默认线路
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/get_domain_default_line',
|
||
data: {domain_id: target.domain_id},
|
||
dataType: 'json',
|
||
success: function(lineRes){
|
||
var line = (lineRes.code === 0 && lineRes.data && lineRes.data.default_line) ? lineRes.data.default_line : '0';
|
||
// 使用获取到的线路添加记录
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/record/add/' + target.domain_id,
|
||
data: {
|
||
name: target.record_name,
|
||
type: 'CNAME',
|
||
value: cnameValue,
|
||
line: line,
|
||
ttl: 600,
|
||
mx: 1,
|
||
weight: 0,
|
||
remark: 'Cloudflare DCV 委派'
|
||
},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
layer.closeAll();
|
||
layer.msg('DCV 委派 CNAME 记录已添加到 ' + (target.domain_name || '-'), {icon: 1, time: 2000});
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('获取默认线路失败', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function hostnameResponseHandler(res){
|
||
if(res.code !== 0){
|
||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||
return {total: 0, rows: []};
|
||
}
|
||
// 保存完整的原始数据用于客户端搜索
|
||
allHostnameData = res.rows || [];
|
||
return res;
|
||
}
|
||
|
||
function refreshHostnameList(){
|
||
$("#hostnameSearchInput").val('');
|
||
$("#listTable").bootstrapTable('refresh');
|
||
}
|
||
|
||
function formatStatus(value){
|
||
var v = String(value || '').toLowerCase();
|
||
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||
}
|
||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||
}
|
||
if(v && v !== '-'){
|
||
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
||
}
|
||
return '-';
|
||
}
|
||
|
||
function getHostnameRow(id){
|
||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||
if(!row){
|
||
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
||
return null;
|
||
}
|
||
return row;
|
||
}
|
||
|
||
function resetHostnameForm(){
|
||
$("#form-store")[0].reset();
|
||
$("#form-store input[name=hostname_id]").val('');
|
||
$("#form-store input[name=hostname]").prop('readonly', false);
|
||
$("#form-store").data("bootstrapValidator").resetForm(true);
|
||
}
|
||
|
||
function openAddDialog(){
|
||
resetHostnameForm();
|
||
$("#storeTitle").text('添加自定义主机名');
|
||
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
||
$("#modal-store").modal('show');
|
||
}
|
||
|
||
function openEditDialog(id){
|
||
var row = getHostnameRow(id);
|
||
if(!row){
|
||
return;
|
||
}
|
||
resetHostnameForm();
|
||
$("#storeTitle").text('编辑自定义主机名');
|
||
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
||
$("#form-store input[name=hostname_id]").val(row.id);
|
||
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
||
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
||
|
||
// 设置当前的证书验证方法
|
||
var sslMethod = 'txt';
|
||
if(row.ssl && row.ssl.method){
|
||
sslMethod = row.ssl.method === 'txt' ? 'txt' : 'http';
|
||
}
|
||
$("#form-store select[name=ssl_method]").val(sslMethod);
|
||
|
||
// 设置当前的最低 TLS 版本
|
||
var minTlsVersion = '1.0';
|
||
if(row.ssl && row.ssl.settings && row.ssl.settings.min_tls_version){
|
||
minTlsVersion = row.ssl.settings.min_tls_version;
|
||
}
|
||
$("#form-store select[name=min_tls_version]").val(minTlsVersion);
|
||
|
||
$("#modal-store").modal('show');
|
||
}
|
||
|
||
function submitHostname(){
|
||
$("#form-store").data("bootstrapValidator").validate();
|
||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||
return;
|
||
}
|
||
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
||
var url = hostnameId ? '/cloudflare/hostnames/update/{$domainId}' : '/cloudflare/hostnames/add/{$domainId}';
|
||
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: url,
|
||
data: $("#form-store").serialize(),
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#modal-store").modal('hide');
|
||
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
||
if(res.data && res.data.id){
|
||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
||
refreshHostnameList();
|
||
}
|
||
}else{
|
||
refreshHostnameList();
|
||
}
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function openVerificationDialog(id){
|
||
var row = getHostnameRow(id);
|
||
if(!row){
|
||
return;
|
||
}
|
||
currentVerificationHostnameId = id;
|
||
renderVerificationDialog(row);
|
||
$("#modal-verification").modal('show');
|
||
}
|
||
|
||
function refreshHostnameValidation(){
|
||
if(!currentVerificationHostnameId){
|
||
layer.msg('请先选择自定义主机名');
|
||
return;
|
||
}
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/refresh/{$domainId}',
|
||
data: {hostname_id: currentVerificationHostnameId},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
if(res.data && res.data.id){
|
||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||
renderVerificationDialog(res.data);
|
||
}else{
|
||
refreshHostnameList();
|
||
}
|
||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderVerificationDialog(row){
|
||
$("#verificationTitle").text('证书验证 - ' + row.hostname);
|
||
var html = '';
|
||
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新验证”会重新向 Cloudflare 发起一次验证。</div>';
|
||
html += '<div class="row">';
|
||
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
||
html += '<div class="col-sm-4">'+renderSummaryCard('证书验证', formatStatusText(row.ssl_validation_status))+'</div>';
|
||
html += '<div class="col-sm-4">'+renderSummaryCard('主机名验证', formatStatusText(row.verification_status))+'</div>';
|
||
html += '</div>';
|
||
|
||
var ownership = row.ownership_verification || {};
|
||
if(ownership.name || ownership.value){
|
||
html += renderSection('主机名 TXT 验证',
|
||
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
||
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
||
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
||
+ renderQuickAddTxtButton(ownership.name || '', ownership.value || '', '快速添加 TXT', true)
|
||
);
|
||
}
|
||
|
||
var ownershipHttp = row.ownership_verification_http || {};
|
||
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
||
html += renderSection('主机名 HTTP 验证',
|
||
renderCopyInput('证书验证请求', ownershipHttp.http_url || '', true)
|
||
+ renderCopyTextarea('证书验证响应', ownershipHttp.http_body || '', true, 3)
|
||
);
|
||
}
|
||
|
||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||
if(records.length > 0){
|
||
var recordsHtml = '';
|
||
for(var i = 0; i < records.length; i++){
|
||
var item = records[i] || {};
|
||
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
||
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
||
recordsHtml += '<div class="panel-heading"><strong>证书验证 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
||
recordsHtml += '<div class="panel-body">';
|
||
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
||
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
||
recordsHtml += renderQuickAddTxtButton(item.txt_name || '', item.txt_value || '', '快速添加 TXT');
|
||
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
||
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
||
recordsHtml += renderCopyTextarea('证书验证请求', item.http_url || '', true, 2);
|
||
recordsHtml += renderCopyTextarea('证书验证响应', item.http_body || '', true, 3);
|
||
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
||
recordsHtml += '</div></div>';
|
||
}
|
||
html += renderSection('证书验证', recordsHtml);
|
||
}else{
|
||
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书验证记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新验证”或稍后刷新列表。</div>';
|
||
}
|
||
|
||
if(row.validation_errors){
|
||
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
||
}
|
||
|
||
$("#verificationContent").html(html);
|
||
}
|
||
|
||
function renderSummaryCard(title, value){
|
||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
||
}
|
||
|
||
function renderSection(title, body){
|
||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
||
}
|
||
|
||
function renderCopyInput(label, value, copyable){
|
||
var safeValue = String(value || '');
|
||
if(!safeValue){
|
||
return '';
|
||
}
|
||
var html = '<div class="form-group">';
|
||
html += '<label>' + htmlEscape(label) + '</label>';
|
||
if(copyable){
|
||
html += '<div class="input-group">';
|
||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
||
html += '</div>';
|
||
}else{
|
||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function renderCopyTextarea(label, value, copyable, rows){
|
||
var safeValue = String(value || '');
|
||
if(!safeValue){
|
||
return '';
|
||
}
|
||
var html = '<div class="form-group">';
|
||
html += '<label>' + htmlEscape(label) + '</label>';
|
||
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
||
if(copyable){
|
||
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function renderQuickAddTxtButton(name, value, label, isHostnameVerification){
|
||
var txtName = String(name || '').trim();
|
||
var txtValue = String(value || '').trim();
|
||
if(!txtName || !txtValue){
|
||
return '';
|
||
}
|
||
return '<div class="text-right" style="margin-top:8px;margin-bottom:12px;"><button type="button" class="btn btn-success btn-xs" data-name="' + encodeURIComponent(txtName) + '" data-value="' + encodeURIComponent(txtValue) + '" data-hostname-verification="' + (isHostnameVerification ? 'true' : 'false') + '" onclick="quickAddTxtRecord(this)">' + htmlEscape(label || '快速添加 TXT') + '</button></div>';
|
||
}
|
||
|
||
function formatStatusText(value){
|
||
var text = value || '-';
|
||
if(text === '-'){
|
||
return '<span class="text-muted">-</span>';
|
||
}
|
||
return formatStatus(text);
|
||
}
|
||
|
||
function copyEncodedValue(btn){
|
||
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
||
}
|
||
|
||
function copyText(text){
|
||
var value = String(text || '');
|
||
if(!value){
|
||
layer.msg('没有可复制的内容');
|
||
return;
|
||
}
|
||
if(navigator.clipboard && window.isSecureContext){
|
||
navigator.clipboard.writeText(value).then(function(){
|
||
layer.msg('已复制', {icon: 1, time: 1000});
|
||
}).catch(function(){
|
||
fallbackCopyText(value);
|
||
});
|
||
return;
|
||
}
|
||
fallbackCopyText(value);
|
||
}
|
||
|
||
function fallbackCopyText(text){
|
||
var $temp = $('<textarea readonly></textarea>');
|
||
$('body').append($temp);
|
||
$temp.val(text).select();
|
||
try{
|
||
document.execCommand('copy');
|
||
layer.msg('已复制', {icon: 1, time: 1000});
|
||
}catch(e){
|
||
layer.alert('复制失败,请手动复制', {icon: 2});
|
||
}
|
||
$temp.remove();
|
||
}
|
||
|
||
function quickAddTxtRecord(btn){
|
||
var fullName = decodeURIComponent($(btn).attr('data-name') || '');
|
||
var value = decodeURIComponent($(btn).attr('data-value') || '');
|
||
var isHostnameVerification = $(btn).attr('data-hostname-verification') === 'true';
|
||
resolveTxtRecordTargets(fullName, function(targets){
|
||
if(!targets.length){
|
||
layer.alert('系统中未找到与该 TXT 主机名对应的托管域名,请手动到解析页添加', {icon: 2});
|
||
return;
|
||
}
|
||
if(targets.length === 1){
|
||
confirmQuickAddTxtRecord(fullName, value, targets[0], isHostnameVerification);
|
||
return;
|
||
}
|
||
openTxtTargetPicker(fullName, value, targets, isHostnameVerification);
|
||
});
|
||
}
|
||
|
||
function resolveTxtRecordTargets(fullName, callback){
|
||
resolveDcvCnameTargets(fullName, function(targets){
|
||
if(targets.length === 0){
|
||
layer.alert('未找到对应的解析域名', {icon: 2});
|
||
}
|
||
callback(targets);
|
||
});
|
||
}
|
||
|
||
function openTxtTargetPicker(fullName, value, targets, isHostnameVerification){
|
||
var isMobile = window.innerWidth <= 768;
|
||
var dialogWidth = isMobile ? '90%' : '640px';
|
||
var html = '<div style="padding:16px 18px 6px;">';
|
||
html += '<div class="alert alert-warning" style="margin-bottom:12px;">检测到多个可用解析域名,请确认要写入哪个服务商。</div>';
|
||
html += '<div class="form-group"><label>TXT 主机名</label><div><code style="word-wrap:break-word;word-break:break-all;display:block;">' + htmlEscape(fullName) + '</code></div></div>';
|
||
html += '<div class="form-group"><label>TXT 值</label><textarea class="form-control" rows="3" readonly style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(value) + '</textarea></div>';
|
||
html += '<form id="txtTargetPickerForm">';
|
||
for(var i = 0; i < targets.length; i++){
|
||
var target = targets[i] || {};
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||
html += '<div class="radio" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:10px 12px;">';
|
||
html += '<label style="display:block;padding-left:22px;word-wrap:break-word;word-break:break-all;">';
|
||
html += '<input type="radio" name="txtTarget" value="' + htmlEscape(String(target.domain_id || '')) + '"' + (i === 0 ? ' checked' : '') + '>';
|
||
html += '<strong style="display:inline-block;max-width:100%;word-wrap:break-word;word-break:break-all;">' + htmlEscape(target.domain_name || '-') + '</strong>';
|
||
if(target.is_current_domain){
|
||
html += ' <span class="label label-primary">当前页</span>';
|
||
}
|
||
html += '<div class="help-block" style="margin:8px 0 0;word-wrap:break-word;word-break:break-all;">';
|
||
html += '主机记录:<code style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(target.record_name || '@') + '</code><br>';
|
||
html += '服务商:' + htmlEscape(providerName) + '<br>';
|
||
html += '账户:<span style="word-wrap:break-word;word-break:break-all;">' + htmlEscape(accountName) + '</span>';
|
||
html += '</div>';
|
||
html += '</label></div>';
|
||
}
|
||
html += '</form></div>';
|
||
layer.open({
|
||
type: 1,
|
||
title: '选择解析服务商',
|
||
area: [dialogWidth, 'auto'],
|
||
shadeClose: false,
|
||
content: html,
|
||
btn: ['添加 TXT', '取消'],
|
||
yes: function(index){
|
||
var selectedId = $('#txtTargetPickerForm input[name=txtTarget]:checked').val();
|
||
var target = findTxtTargetByDomainId(targets, selectedId);
|
||
if(!target){
|
||
layer.msg('请选择要写入的解析域名', {icon: 2});
|
||
return;
|
||
}
|
||
layer.close(index);
|
||
submitQuickAddTxtRecord(value, target, isHostnameVerification);
|
||
}
|
||
});
|
||
}
|
||
|
||
function confirmQuickAddTxtRecord(fullName, value, target, isHostnameVerification){
|
||
layer.confirm(buildQuickAddConfirmHtml(fullName, value, target), {title: '提示', icon: 0}, function(index){
|
||
layer.close(index);
|
||
submitQuickAddTxtRecord(value, target, isHostnameVerification);
|
||
});
|
||
}
|
||
|
||
function buildQuickAddConfirmHtml(fullName, value, target){
|
||
var providerName = target.account_type_name || target.account_type || '-';
|
||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||
return '<div style="max-width:100%;overflow:hidden;">'
|
||
+ '<p style="margin-bottom:10px;">确定要快速添加 TXT 记录吗?</p>'
|
||
+ '<div style="background:#f5f5f5;padding:10px;border-radius:4px;border:1px solid #e5e5e5;font-size:13px;">'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">记录类型</span><span style="color:#d9534f;font-weight:bold;">TXT</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">TXT 主机名</span><span style="word-wrap:break-word;word-break:break-all;display:inline-block;vertical-align:top;max-width:calc(100% - 80px);color:#337ab7;font-family:Consolas,Monaco,Courier New,monospace;background:#fff;padding:2px 6px;border-radius:3px;border:1px solid #ddd;">' + htmlEscape(fullName) + '</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">TXT 值</span><span style="word-wrap:break-word;word-break:break-all;display:inline-block;vertical-align:top;max-width:calc(100% - 80px);color:#337ab7;font-family:Consolas,Monaco,Courier New,monospace;background:#fff;padding:2px 6px;border-radius:3px;border:1px solid #ddd;">' + htmlEscape(value) + '</span></div>'
|
||
+ '<div style="margin-bottom:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">解析域名</span><span>' + htmlEscape(target.domain_name || '-') + '</span></div>'
|
||
+ '<div style="margin-bottom:0;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">主机记录</span><span style="word-wrap:break-word;word-break:break-all;display:inline-block;vertical-align:top;max-width:calc(100% - 80px);color:#337ab7;font-family:Consolas,Monaco,Courier New,monospace;background:#fff;padding:2px 6px;border-radius:3px;border:1px solid #ddd;">' + htmlEscape(target.record_name || '@') + '</span></div>'
|
||
+ '<div style="margin-top:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">服务商</span><span>' + htmlEscape(providerName) + '</span></div>'
|
||
+ '<div style="margin-top:6px;"><span style="display:inline-block;width:70px;min-width:70px;font-weight:bold;color:#555;">账户</span><span>' + htmlEscape(accountName) + '</span></div>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
}
|
||
|
||
function submitQuickAddTxtRecord(value, target, isHostnameVerification){
|
||
var ii = layer.load(2);
|
||
// 区分是主机名验证还是证书验证的 TXT 记录
|
||
var remark = isHostnameVerification ? 'Cloudflare 主机名验证' : 'Cloudflare 证书验证';
|
||
// 先获取目标域名的默认线路
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/get_domain_default_line',
|
||
data: {domain_id: target.domain_id},
|
||
dataType: 'json',
|
||
success: function(lineRes){
|
||
var line = (lineRes.code === 0 && lineRes.data && lineRes.data.default_line) ? lineRes.data.default_line : '0';
|
||
// 使用获取到的线路添加记录
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/record/add/' + target.domain_id,
|
||
data: {
|
||
name: target.record_name,
|
||
type: 'TXT',
|
||
value: value,
|
||
line: line,
|
||
ttl: 600,
|
||
mx: 1,
|
||
weight: 0,
|
||
remark: remark
|
||
},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
layer.closeAll();
|
||
$("#modal-verification").modal('show');
|
||
layer.msg('TXT 记录已添加到 ' + (target.domain_name || '-'), {icon: 1, time: 1400});
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('获取默认线路失败', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function findTxtTargetByDomainId(targets, domainId){
|
||
var selected = String(domainId || '');
|
||
for(var i = 0; i < targets.length; i++){
|
||
var item = targets[i] || {};
|
||
if(String(item.domain_id || '') === selected){
|
||
return item;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function deleteHostname(id, hostname){
|
||
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/hostnames/delete/{$domainId}',
|
||
data: {hostname_id: id, hostname: hostname},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
layer.closeAll();
|
||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||
refreshHostnameList();
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function loadFallbackOrigin(){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/fallback/get/{$domainId}',
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#fallbackOrigin").val((res.data && res.data.origin) ? res.data.origin : '');
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function saveFallbackOrigin(){
|
||
var origin = $.trim($("#fallbackOrigin").val());
|
||
if(!origin){
|
||
layer.msg('请输入 Fallback Origin');
|
||
return;
|
||
}
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/fallback/set/{$domainId}',
|
||
data: {origin: origin},
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
$("#fallbackOrigin").val(res.data.origin || origin);
|
||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
}
|
||
|
||
function clearFallbackOrigin(){
|
||
layer.confirm('确定要清空 Fallback Origin 吗?', {title: '提示', icon: 0}, function(){
|
||
var ii = layer.load(2);
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: '/cloudflare/fallback/delete/{$domainId}',
|
||
dataType: 'json',
|
||
success: function(res){
|
||
layer.close(ii);
|
||
if(res.code === 0){
|
||
layer.closeAll();
|
||
$("#fallbackOrigin").val('');
|
||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||
}else{
|
||
layer.alert(res.msg, {icon: 2});
|
||
}
|
||
},
|
||
error: function(){
|
||
layer.close(ii);
|
||
layer.alert('服务器错误', {icon: 2});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function htmlEscape(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
</script>
|
||
{/block}
|