新增青云DNS

This commit is contained in:
net909 2026-01-23 23:43:01 +08:00
parent 86c557face
commit 6aea445259
12 changed files with 968 additions and 102 deletions

View File

@ -457,6 +457,9 @@ class Domain extends BaseController
View::assign('recordLine', $recordLineArr);
View::assign('minTTL', $minTTL ? $minTTL : 1);
View::assign('dnsconfig', $dnsconfig);
if ($dnstype == 'qingcloud') {
return view('qingcloud');
}
return view();
}

View File

@ -287,6 +287,41 @@ class DnsHelper
'page' => false,
'add' => true,
],
'qingcloud' => [
'name' => '青云',
'icon' => 'qingcloud.ico',
'note' => '',
'config' => [
'access_key_id' => [
'name' => 'Access Key ID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'secret_access_key' => [
'name' => 'Secret Access Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
'redirect' => false,
'log' => false,
'weight' => true,
'page' => false,
'add' => true,
],
'bt' => [
'name' => '宝塔域名',
'icon' => 'bt.png',
@ -339,6 +374,15 @@ class DnsHelper
'placeholder' => '',
'required' => true,
],
'auth' => [
'name' => '认证方式',
'type' => 'radio',
'options' => [
'0' => 'API密钥',
'1' => 'API令牌',
],
'value' => '0'
],
'apikey' => [
'name' => 'API密钥/令牌',
'type' => 'input',

View File

@ -8,6 +8,7 @@ class cloudflare implements DnsInterface
{
private $Email;
private $ApiKey;
private $auth;
private $baseUrl = 'https://api.cloudflare.com/client/v4';
private $error;
private $domain;
@ -21,6 +22,7 @@ class cloudflare implements DnsInterface
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->auth = isset($config['auth']) ? intval($config['auth']) : (preg_match('/^[0-9a-f]+$/i', $this->ApiKey) ? 0 : 1);
}
public function getError()
@ -75,7 +77,7 @@ class cloudflare implements DnsInterface
if ($data) {
$list = [];
foreach ($data['result'] as $row) {
$name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']);
$name = $this->domain == $row['name'] ? '@' : substr($row['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
@ -112,7 +114,7 @@ class cloudflare implements DnsInterface
{
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
if ($data) {
$name = $this->domain == $data['result']['name'] ? '@' : str_replace('.' . $this->domain, '', $data['result']['name']);
$name = $this->domain == $data['result']['name'] ? '@' : substr($data['result']['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
@ -263,14 +265,14 @@ class cloudflare implements DnsInterface
{
$url = $this->baseUrl . $path;
if (preg_match('/^[0-9a-f]+$/i', $this->ApiKey)) {
if ($this->auth == 0) {
$headers = [
'X-Auth-Email: ' . $this->Email,
'X-Auth-Key: ' . $this->ApiKey,
'X-Auth-Email' => $this->Email,
'X-Auth-Key' => $this->ApiKey,
];
} else {
$headers = [
'Authorization: Bearer ' . $this->ApiKey,
'Authorization' => 'Bearer ' . $this->ApiKey,
];
}
@ -281,39 +283,17 @@ class cloudflare implements DnsInterface
}
} else {
$body = json_encode($params);
$headers[] = 'Content-Type: application/json';
$headers['Content-Type'] = 'application/json';
}
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
try {
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
$arr = json_decode($response, true);
$arr = json_decode($response['body'], true);
if ($arr) {
if ($arr['success']) {
return $arr;

View File

@ -235,7 +235,10 @@ class dnsla implements DnsInterface
private function execute($method, $path, $params = null)
{
$token = base64_encode($this->apiid.':'.$this->apisecret);
$header = ['Authorization: Basic '.$token, 'Content-Type: application/json; charset=utf-8'];
$header = [
'Authorization' => 'Basic '.$token,
'Content-Type' => 'application/json; charset=utf-8'
];
if ($method == 'POST' || $method == 'PUT') {
$response = $this->curl($method, $path, $header, json_encode($params));
} else {
@ -264,34 +267,19 @@ class dnsla implements DnsInterface
private function curl($method, $path, $header, $body = null)
{
$url = $this->baseUrl . $path;
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
try {
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) return false;
if ($httpCode == 200) {
return $response;
} elseif ($httpCode == 401) {
if ($response['code'] == 200) {
return $response['body'];
} elseif ($response['code'] == 401) {
$this->setError('认证失败');
return false;
} else {
$this->setError('http code: '.$httpCode);
$this->setError('http code: '.$response['code']);
return false;
}
}

View File

@ -63,11 +63,10 @@ class namesilo implements DnsInterface
if ($data) {
$list = [];
foreach ($data['resource_record'] as $row) {
$name = $row['host'] == $this->domain ? '@' : str_replace('.'.$this->domain, '', $row['host']);
$list[] = [
'RecordId' => $row['record_id'],
'Domain' => $this->domain,
'Name' => $name,
'Name' => $row['host'],
'Type' => $row['type'],
'Value' => $row['value'],
'Line' => 'default',

386
app/lib/dns/qingcloud.php Normal file
View File

@ -0,0 +1,386 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class qingcloud implements DnsInterface
{
private $access_key_id;
private $secret_access_key;
private $baseUrl = 'http://api.routewize.com';
private $error;
private $domain;
private $domainid;
private $proxy;
public function __construct($config)
{
$this->access_key_id = $config['access_key_id'];
$this->secret_access_key = $config['secret_access_key'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$offset = ($PageNumber - 1) * $PageSize;
$param = ['offset' => $offset, 'limit' => $PageSize];
if (!empty($KeyWord)) {
$param['zone_name'] = $KeyWord;
}
$data = $this->execute('GET', '/v1/user/zones', $param);
if ($data) {
$list = [];
foreach ($data['zones'] as $row) {
$list[] = [
'DomainId' => $row['zone_name'],
'Domain' => rtrim($row['zone_name'], '.'),
'RecordCount' => 0,
];
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
if ($SubDomain) {
return $this->getHostRecords($SubDomain);
}
$param = ['zone_name' => $this->domainid, 'offset' => 0, 'limit' => 100];
if (!isNullOrEmpty($KeyWord)) {
$param['search_word'] = $KeyWord;
}
$data = $this->execute('GET', '/v1/dns/host/', $param);
if ($data) {
$list = [];
foreach ($data['domains'] as $row) {
$name = substr($row['domain_name'], 0, -(strlen($row['zone_name']) + 1));
if ($name == '') $name = '@';
$list[] = [
'RecordId' => $row['domain_name'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => null,
'Value' => null,
'Line' => null,
'TTL' => null,
'MX' => null,
'Status' => $row['status'] == 'enabled' ? '0' : '1',
'Weight' => null,
'Remark' => $row['description'],
'UpdateTime' => $row['create_time'],
'Count' => $row['count'],
];
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
private function getHostRecords($SubDomain)
{
$param = ['zone_name' => $this->domainid, 'domain_name' => $SubDomain];
$data = $this->execute('GET', '/v1/dns/host_info/', $param);
if ($data) {
$list = [];
foreach ($data['records'] as $record) {
$name = substr($record['domain_name'], 0, -(strlen($record['zone_name']) + 1));
if ($name == '') $name = '@';
foreach ($record['record'] as $record_group) {
foreach ($record_group['data'] as $row) {
$mx = null;
if ($record['rd_type'] == 'MX') {
$value = explode(' ', $row['value'], 2);
$row['value'] = isset($value[1]) ? $value[1] : '';
$mx = intval($value[0]);
}
if ($record['rd_type'] == 'TXT') {
$row['value'] = trim($row['value'], '"');
}
$list[] = [
'RecordId' => $record['domain_record_id'].'_'.$row['record_value_id'],
'Domain' => $record['domain_name'],
'Name' => $name,
'Type' => $record['rd_type'],
'Mode' => $record['mode'],
'Value' => $row['value'],
'Line' => $record['view_id'],
'TTL' => $record['ttl'],
'MX' => $mx,
'Status' => $row['status'] == 1 ? '1' : '0',
'Weight' => $record_group['weight'] > 0 ? $record_group['weight'] : null,
'Remark' => null,
'UpdateTime' => $record['create_time'],
];
}
}
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
$SubDomain = $this->getHost($SubDomain);
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$mode = input('post.mode', '1');
if ($Type == 'MX') {
$Value = intval($MX).' '.$Value;
} elseif ($Type == 'TXT') {
$Value = '"'.$Value.'"';
}
$values = [];
foreach (explode(',', $Value) as $val) {
$values[] = ['value' => trim($val), 'status' => 1];
}
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
else $Weight = 0;
$record = [['weight' => $Weight, 'values' => $values]];
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode), 'auto_merge' => 2];
$data = $this->execute('POST', '/v1/record/', $param);
return is_array($data) ? $data['domain_record_id'] : false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$mode = input('post.mode', '1');
if ($Type == 'MX') {
$Value = intval($MX).' '.$Value;
} elseif ($Type == 'TXT') {
$Value = '"'.$Value.'"';
}
$recordId = explode('_', $RecordId);
$domain_record_id = $recordId[0];
$record_value_id = $recordId[1];
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
if (!$data) return false;
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
else $Weight = 0;
$record = [];
foreach ($data['data']['record'] as $record_group) {
$values = [];
$flag = false;
foreach ($record_group['data'] as $row) {
if ($row['record_value_id'] == $record_value_id) {
$row['value'] = $Value;
$flag = true;
}
$values[] = ['value' => $row['value'], 'status' => $row['status']];
}
if (count($values) > 0) {
$record[] = ['weight' => $flag ? $Weight : $record_group['weight'], 'values' => $values];
}
}
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode)];
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
return $data !== false;
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
$param = ['zone_name' => $this->domainid, 'domain_name' => $RecordId, 'description' => $Remark];
$data = $this->execute('POST', '/v1/dns/host/', $param);
return $data !== false;
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
if (strpos($RecordId, $this->domainid) !== false) {
$param = ['domain_names' => json_encode([$RecordId]), 'zone_name' => $this->domainid];
$data = $this->execute('DELETE', '/v1/domain/', $param);
return $data !== false;
}
$recordId = explode('_', $RecordId);
$domain_record_id = $recordId[0];
$record_value_id = $recordId[1];
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
if (!$data) return false;
$record = [];
foreach ($data['data']['record'] as $record_group) {
$values = [];
foreach ($record_group['data'] as $row) {
if ($row['record_value_id'] == $record_value_id) {
continue;
}
$values[] = ['value' => $row['value'], 'status' => $row['status']];
}
if (count($values) > 0) {
$record[] = ['weight' => $record_group['weight'], 'values' => $values];
}
}
if (count($record) == 0) {
$param = ['ids' => json_encode([$domain_record_id]), 'target' => 'record', 'action' => 'delete'];
$data = $this->execute('POST', '/v1/change_record_status/', $param);
return $data !== false;
}
$name = substr($data['data']['domain_name'], 0, -(strlen($data['data']['zone_name']) + 1));
if ($name == '') $name = '@';
$param = ['zone_name' => $this->domainid, 'domain_name' => $name, 'view_id' => $data['data']['view_id'], 'type' => $data['data']['rd_type'], 'ttl' => $data['data']['ttl'], 'record' => json_encode($record), 'mode' => $data['data']['mode']];
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
return $data !== false;
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
$recordId = explode('_', $RecordId);
$record_value_id = $recordId[1];
$param = ['ids' => json_encode([$record_value_id]), 'target' => 'value', 'action' => $Status == '0' ? 'stop' : 'enable'];
$data = $this->execute('POST', '/v1/change_record_status/', $param);
return $data !== false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
$param = ['zone_name' => $this->domainid, 'type' => 'GET_FULL'];
$data = $this->execute('GET', '/v1/zone/view/', $param);
if ($data) {
$list = [];
foreach ($data['zone_views'] as $row) {
if ($row['name'] == '*') $row['name'] = '默认';
$list[$row['id']] = ['name' => $row['name'], 'parent' => null];
}
return $list;
}
return false;
}
//获取域名信息
public function getDomainInfo()
{
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
return 60;
}
public function addDomain($Domain)
{
$param = ['zone_name' => $Domain];
$data = $this->execute('POST', '/v1/zone/', $param);
if ($data) {
return ['id' => $data['zone_name'], 'name' => $Domain];
}
return false;
}
private function getHost($Name)
{
if ($Name == '@' || $Name == '') $Name = '';
else $Name .= '.';
$Name .= $this->domain . '.';
return $Name;
}
private function execute($method, $path, $params = null)
{
$date = gmdate('D, d M Y H:i:s \G\M\T');
$string_to_sign = $method."\n".$date."\n".$path;
if($method == 'GET' && $params){
ksort($params);
$string_to_sign .= '?'.http_build_query($params);
}
$signature = base64_encode(hash_hmac('sha256', $string_to_sign, $this->secret_access_key, true));
$authorization = 'QC-HMAC-SHA256 '.$this->access_key_id.':'.$signature;
$header = [
'Authorization' => $authorization,
'Date' => $date,
];
if ($method == 'POST' || $method == 'PUT' || $method == 'DELETE') {
$header['Content-Type'] = 'application/json; charset=utf-8';
$response = $this->curl($method, $path, $header, json_encode($params));
} else {
if ($params) {
$path .= '?'.http_build_query($params);
}
$response = $this->curl($method, $path, $header);
}
$arr = json_decode($response['body'], true);
if (isset($arr['code']) && $arr['code'] == 0 || isset($arr['domains']) || $method == 'DELETE' && $response['code'] == 204) {
return $arr;
} elseif(isset($arr['message'])) {
$this->setError($arr['message']);
return false;
} elseif(isset($arr['msg'])) {
$this->setError($arr['msg']);
return false;
} else {
$this->setError('返回数据解析失败');
return false;
}
}
private function curl($method, $path, $header, $body = null)
{
$url = $this->baseUrl . $path;
try {
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
return $response;
}
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@ -76,9 +76,7 @@ $(document).ready(function(){
})
})
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此域名账户吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此域名账户吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -95,8 +93,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
</script>

View File

@ -428,9 +428,7 @@ function saveEdit(){
});
}
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -447,8 +445,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function getDomainList(){
@ -517,9 +513,7 @@ function operation(action){
window.location.href = '/record/batchedit';
return;
}else if(action == 'delete'){
var confirmobj = layer.confirm('确定要删除所选域名吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除所选域名吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -537,13 +531,9 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}else if(action == 'updateexpire'){
var confirmobj = layer.confirm('提交后将异步刷新所选域名的到期时间', {
btn: ['确定','取消']
}, function(){
layer.confirm('提交后将异步刷新所选域名的到期时间', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -561,8 +551,6 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}else{
var is_notice = action == 'opennotice' ? 1 : 0;

View File

@ -0,0 +1,493 @@
{extend name="common/layout" /}
{block name="title"}解析管理 - {$domainName}{/block}
{block name="main"}
<style>
td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;}
.dns-parent-row { cursor: pointer; }
.dns-child-row td { background: #fafafa; }
.dns-child-row td:nth-child(4) { padding-left: 24px; }
.dns-child-empty td { background:#fafafa; }
.glyphicon-spin { animation: spin 1s infinite linear; }
@keyframes spin { from {transform:rotate(0deg);} to {transform:rotate(360deg);} }
.form-group .radio-inline {position: unset;}
.tips {color: #f6a838;padding-left: 5px;}
.text-remark {margin-left: 10px;color: #329a29;font-size: 12px;}
</style>
<div class="row" id="app">
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" 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
aria-hidden="true">&times;</span><span
class="sr-only">Close</span></button>
<h4 class="modal-title" id="modal-title">{{form.action=='add'?'添加解析':'修改解析'}}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" id="form-store">
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">主机记录</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="name" placeholder="填写域名前缀,支持多级" v-model="form.name" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">记录类型</label>
<div class="col-sm-9">
<select name="type" class="form-control" v-model="form.type">
<option value="A">A</option>
<option value="CNAME">CNAME</option>
<option value="AAAA">AAAA</option>
<option value="NS">NS</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">线路类型</label>
<div class="col-sm-9" id="line_list">
<select name="line" class="form-control" v-model="form.line">
<option v-for="line in recordLine" :value="line.id">{{line.name}}</option>
</select>
</div>
</div>
<div class="form-group" v-show="form.type=='A' || form.type=='CNAME'">
<label class="col-sm-3 control-label">模式</label>
<div class="col-sm-9" id="line_list">
<label class="radio-inline"><input type="radio" name="mode" value="1" v-model="form.mode"> 普通<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将按您填写的顺序返回解析结果,查询性能更好。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="2" v-model="form.mode"> 轮询<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询的解析结果排序都将会较上一次发生变化,业务负载更均衡。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="4" v-model="form.mode"> 智能<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="根据访问来源的运营商及地理位置将解析结果按匹配度排序并最多返回前 5 个,可减少您对精细化线路配置的烦恼。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline"><input type="radio" name="mode" value="3" v-model="form.mode"> 权重<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将根据每组解析结果的权值按比例返回,使得业务负载可以随心所欲。" class="tips"><i class="fa fa-question-circle"></i></span></label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">记录值</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="value" :placeholder="'输入记录值' + (form.type=='A'&&form.action=='add'?'多个IP用,间隔)':'')" v-model="form.value" required>
</div>
</div>
<div class="form-group" v-show="form.type=='MX'">
<label class="col-sm-3 control-label no-padding-right">MX优先级</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="mx" v-model="form.mx">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">TTL</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="ttl" v-model="form.ttl" placeholder="指解析结果在DNS服务器中的缓存时间" required min="{$minTTL}">
</div>
</div>
<div class="form-group" v-show="(form.type=='A' || form.type=='CNAME') && form.mode=='3'">
<label class="col-sm-3 control-label no-padding-right">权重</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="weight" v-model="form.weight" placeholder="权重值(1-99)" min="1" max="99">
</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" id="store" @click="save">保存</button>
</div>
</div>
</div>
</div>
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{if request()->user['type'] eq 'user'}<a href="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{/if}{$domainName}</h3>
</div>
<div class="panel-body">
<form class="form-inline" id="searchToolbar" @submit.prevent>
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="keyword" placeholder="输入主机记录" v-model="keyword" @keyup.enter="loadParents">
</div>
<button type="button" class="btn btn-primary" @click="loadParents"><i class="fa fa-search"></i> 搜索</button>
<button type="button" class="btn btn-default" title="刷新解析记录列表" @click="keyword=null;loadParents()"><i class="fa fa-refresh"></i> 刷新</button>
<button type="button" class="btn btn-success" @click="addRecord"><i class="fa fa-plus"></i> 添加记录</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="/record/batchadd/{$domainId}">添加</a></li></ul>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">日志 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="/log?domain={$domainName}">本站日志</a></li></ul>
</div>
</form>
<div class="table-responsive" style="margin-top:15px;">
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>主机记录</th>
<th>线路类型</th>
<th>记录类型</th>
<th>模式</th>
<th>记录值</th>
<th>TTL</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<tr>
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
</tr>
</template>
<template v-if="!loading && parents.length === 0">
<tr>
<td colspan="8" class="text-muted text-center">暂无记录</td>
</tr>
</template>
<template v-if="!loading" v-for="p in parents">
<tr :key="'p-' + p.RecordId" class="dns-parent-row" @click="toggleParent(p)">
<td colspan="7">
<span class="text-muted" style="display:inline-block;width:18px;">
<i class="glyphicon"
:class="expandedMap[p.RecordId] ? 'glyphicon-chevron-down' : 'glyphicon-chevron-right'"></i>
</span>
<strong>{{ p.Name }}</strong><span class="text-muted">(共 {{ p.Count }} 条记录)</span><span class="text-remark" v-if="p.Remark"><i class="glyphicon glyphicon-list-alt"></i> {{ p.Remark }}</span>
</td>
<td @click.stop>
<button class="btn btn-xs btn-success" @click="addRecord(p)">添加</button>
<button class="btn btn-xs btn-info" @click="editHostRemark(p)">备注</button>
<button class="btn btn-xs btn-danger" @click="deleteHost(p)">删除</button>
</td>
</tr>
<tr v-if="expandedMap[p.RecordId] && loadingMap[p.RecordId]" class="dns-child-empty">
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
</tr>
<tr v-for="c in (expandedMap[p.RecordId] ? (childrenMap[p.RecordId] || []) : [])"
:key="'c-' + c.RecordId"
v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId]"
class="dns-child-row">
<td></td>
<td>{{ c.LineName }}</td>
<td>{{ c.Type }}</td>
<td>{{ c.Type == 'A' || c.Type == 'CNAME' ? modeList[c.Mode] : '-' }} <span class="label label-info" v-if="(c.Type == 'A' || c.Type == 'CNAME') && c.Mode == 3">{{c.Weight}}</span></td>
<td>{{ c.Value + (c.Type == 'MX' ? ' | ' + c.MX : '') }}<a href="javascript:void(0);" title="复制记录值" @click="copyToClipboard(c, $event)" style="padding-left:6px;"><i class="fa fa-copy"></i></a></td>
<td>{{ c.TTL }}</td>
<td><font color="green" v-if="c.Status=='1'"><i class="fa fa-check-circle"></i>启用</font><font color="orange" v-if="c.Status!='1'"><i class="fa fa-pause-circle"></i>暂停</font></td>
<td>
<button class="btn btn-xs btn-primary" @click="editRecord(c, p.RecordId)">修改</button>
<button class="btn btn-xs btn-warning" @click="setRecordStatus(c, p.RecordId, '0')" v-if="c.Status=='1'">暂停</button>
<button class="btn btn-xs btn-success" @click="setRecordStatus(c, p.RecordId, '1')" v-if="c.Status!='1'">启用</button>
<button class="btn btn-xs btn-danger" @click="deleteRecord(c, p.RecordId)">删除</button>
</td>
</tr>
<tr v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId] && (childrenMap[p.RecordId] || []).length === 0"
:key="'empty-' + p.RecordId"
class="dns-child-empty">
<td colspan="8" class="text-muted text-center">暂无记录</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="/static/js/vue-2.7.16.min.js"></script>
<script src="/static/js/layer/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var recordLine = {$recordLine|json_encode|raw};
var dnsconfig = {$dnsconfig|json_encode|raw};
var defaultLine = recordLine[0].id;
new Vue({
el: '#app',
data: function () {
return {
loading: false,
recordLine: recordLine,
form: {
action: '',
recordid: '',
recordinfo: '',
parentid: '',
name: '',
type: '',
line: '',
mode: '',
value: '',
ttl: 600,
weight: '',
mx: 10,
},
keyword: '',
parents: [],
expandedMap: {}, // {pid: true/false}
loadingMap: {}, // {pid: true/false}
childrenMap: {}, // {pid: []} 缓存子列表
modeList: [
'默认',
'普通',
'轮询',
'权重',
'智能',
]
};
},
mounted: function () {
this.loadParents();
$('[data-toggle="tooltip"]').tooltip();
$("#form-store").bootstrapValidator();
},
methods: {
loadParents: function () {
var vm = this;
vm.loading = true;
vm.expandedMap = {};
vm.childrenMap = {};
$.ajax({
url: '/record/data/{$domainId}',
method: 'POST',
data: { keyword: vm.keyword },
dataType: 'json'
}).done(function (res) {
vm.loading = false;
vm.parents = (res && res.rows) ? res.rows : [];
}).fail(function () {
layer.msg('加载父级列表失败');
});
},
toggleParent: function (p) {
var pid = p.RecordId;
// 收起
if (this.expandedMap[pid]) {
this.$set(this.expandedMap, pid, false);
return;
}
// 展开
this.$set(this.expandedMap, pid, true);
// 已有缓存就不再请求
if (this.childrenMap[pid]) return;
this.loadChildren(pid);
},
loadChildren: function (pid) {
var vm = this;
vm.$set(vm.loadingMap, pid, true);
$.ajax({
url: '/record/data/{$domainId}',
method: 'POST',
data: { subdomain: pid },
dataType: 'json'
}).done(function (res) {
vm.$set(vm.childrenMap, pid, (res && res.rows) ? res.rows : []);
}).fail(function () {
layer.msg('加载子级列表失败');
vm.$set(vm.childrenMap, pid, []);
}).always(function () {
vm.$set(vm.loadingMap, pid, false);
});
},
addRecord: function (p) {
this.form.action = 'add';
this.form.recordid = '';
this.form.recordinfo = '';
this.form.parentid = p.RecordId || '';
this.form.name = p.Name || '';
this.form.type = 'A';
this.form.line = defaultLine;
this.form.mode = '1';
this.form.value = '';
this.form.ttl = 600;
this.form.weight = '';
this.form.mx = 10;
$("#modal-store").modal('show');
$("#form-store").data("bootstrapValidator").resetForm();
},
editRecord: function (c, parentid) {
this.form.action = 'update';
this.form.recordid = c.RecordId;
this.form.recordinfo = JSON.stringify(c);
this.form.parentid = parentid || '';
this.form.name = c.Name;
this.form.type = c.Type;
this.form.line = c.Line;
this.form.mode = c.Mode;
this.form.value = c.Value;
this.form.ttl = c.TTL;
this.form.weight = c.Weight;
this.form.mx = c.MX || 10;
$("#modal-store").modal('show');
$("#form-store").data("bootstrapValidator").resetForm();
},
save: function () {
$("#form-store").data("bootstrapValidator").validate();
if(!$("#form-store").data("bootstrapValidator").isValid()){
return;
}
var vm = this;
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/'+vm.form.action+'/{$domainId}',
data : vm.form,
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg,{
icon: 1,
closeBtn: false
}, function(){
layer.closeAll();
$("#modal-store").modal('hide');
if(vm.form.parentid){
vm.loadChildren(vm.form.parentid);
}else{
vm.loadParents();
}
});
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
},
setRecordStatus: function (c, parentid, status) {
var vm = this;
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/status/{$domainId}',
data : { recordid: c.RecordId, status: status, recordinfo: JSON.stringify(c) },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg(status=='1'?'开启成功':'暂停成功', {icon: 1, time:500});
vm.loadChildren(parentid);
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
},
deleteRecord: function (c, parentid) {
var vm = this;
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/delete/{$domainId}',
data : { recordid: c.RecordId, recordinfo: JSON.stringify(c) },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('删除成功', {icon: 1, time:800});
vm.loadChildren(parentid);
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
});
},
deleteHost: function (p) {
var vm = this;
layer.confirm('确定要删除此主机名下所有解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/delete/{$domainId}',
data : { recordid: p.RecordId },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('删除成功', {icon: 1, time:800});
vm.loadParents();
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
});
},
editHostRemark: function (p) {
var vm = this;
layer.open({
type: 1,
area: ['350px'],
closeBtn: 2,
title: '编辑备注',
content: '<div style="padding:15px"><div class="form-group"><input class="form-control" type="text" name="remark" value="'+(p.Remark==null?'':p.Remark)+'" autocomplete="off" placeholder="备注信息"></div></div>',
btn: ['确认', '取消'],
yes: function(){
var remark = $("input[name='remark']").val();
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/record/remark/{$domainId}',
data : {recordid:p.RecordId, remark:remark},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('保存成功', {icon: 1, time:800});
vm.loadParents();
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
});
},
copyToClipboard: function (c, event) {
var text = c.Value;
var tempInput = document.createElement('input');
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
$(event.target).toggleClass('fa-copy fa-check');
setTimeout(function(){
$(event.target).toggleClass('fa-check fa-copy');
}, 1000);
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
},
}
});
</script>
{/block}

View File

@ -292,7 +292,6 @@ $(document).ready(function(){
formatter: function(value, row, index) {
var copyId = 'copy-value-' + row.RecordId;
if(row.Type == 'MX') {
// 只复制 mx.yandex.net按钮在其右侧优先级单独显示
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>'
+ '<span class="mx-priority"> | '+row.MX+'</span>';
@ -349,10 +348,12 @@ $(document).ready(function(){
if(dnsconfig.remark == 1){
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>&nbsp;&nbsp;';
}
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
if(row.Type == 'A' || row.Type == 'CNAME' || row.Type == 'AAAA' || row.Type == 'REDIRECT_URL' || row.Type == 'FORWARD_URL'){
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
}
return html;
}
},
@ -511,9 +512,7 @@ function setStatus(recordid, status){
}
function delItem(recordid) {
var row = $("#listTable").bootstrapTable('getRowByUniqueId', recordid);
var confirmobj = layer.confirm('确定要删除此解析记录吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -531,8 +530,6 @@ function delItem(recordid) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function setRemark(recordid) {
@ -587,9 +584,7 @@ function operation(action){
return;
}
var confirmobj = layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -607,8 +602,6 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function batch_edit(records){

View File

@ -279,9 +279,7 @@ function setStatus(id,status) {
});
}
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此用户吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此用户吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -298,8 +296,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
var CreatePassword = function (len)

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B