mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-02 11:56:27 +02:00
* Update RewriteRule in .htaccess for cleaner routing 修复Apache环境下路由重写规则 废弃旧版 index.php/$1 写法,改用兼容新版PHP的PATH_INFO传参方式 解决访问时报错 No input file specified. 问题 * Add files via upload 1.添加DCV 委派一键添加CNAME 2.添加证书验证方法和最低 TLS 版本 3.添加批量添加 修改 删除 4.修复华为云一键txt解析失败(我没其他dns, 其他的需关注) 5.Cloudflare增强改Cloudflare自定义主机名 * 1.添加快速解析 2.Cloudflare自定义主机名添加搜索功能 * Add files via upload 1.Cloudflare自定义主机名自动获取默认线路(支持所有dns,华为云退回之前) 2.优化手机上显示问题 3.一键添加 DCV 委派支持选择要写入的解析域名 * 优化手机显示 * 添加1. 批量 DCV 委派 2. 批量主机名 TXT 验证 3. 批量证书 TXT 验证 4. 批量刷新验证 1. 批量 DCV 委派 2. 批量主机名 TXT 验证 3. 批量证书 TXT 验证 4. 批量刷新验证 * 快速解析改名智能解析,添加已有解析记录和智能批量添加 * 快速解析改名智能解析,添加已有解析记录和智能批量添加 * 由于之前复制保存的,代码有些差异 * 修复已有解析记录的备注功能 * 备注按dns显示 * 修复记录值过长无法复制,优化显示 * 优化显示
2549 lines
99 KiB
HTML
2549 lines
99 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="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-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 id="batchDcvTargetPickerList"></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="confirmBatchDcvTargetSelection()">确定</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-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',
|
||
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: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);
|
||
}
|
||
|
||
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 = [];
|
||
var batchDcvSelectedTarget = null;
|
||
var batchDcvCnameValue = '';
|
||
|
||
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();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 提交批量 TXT 记录的通用函数
|
||
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}
|