This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
Vue.component("ns-access-log-box", {
props: ["v-access-log", "v-keyword"],
data: function () {
let accessLog = this.vAccessLog
return {
accessLog: accessLog
}
},
methods: {
showLog: function () {
let that = this
let requestId = this.accessLog.requestId
this.$parent.$children.forEach(function (v) {
if (v.deselect != null) {
v.deselect()
}
})
this.select()
teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, {
width: "50em",
height: "24em",
onClose: function () {
that.deselect()
}
})
},
select: function () {
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
},
deselect: function () {
this.$refs.box.parentNode.style.cssText = ""
}
},
template: `<div class="access-log-row" :style="{'color': (!accessLog.isRecursive && (accessLog.nsRecordId == null || accessLog.nsRecordId == 0) || (accessLog.isRecursive && accessLog.recordValue != null && accessLog.recordValue.length == 0)) ? '#dc143c' : ''}" ref="box">
<span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey">[{{accessLog.region}}]</span> <keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword> [{{accessLog.timeLocal}}] [{{accessLog.networking}}] <em>{{accessLog.questionType}} <keyword :v-word="vKeyword">{{accessLog.questionName}}</keyword></em> -&gt;
<span v-if="accessLog.recordType != null && accessLog.recordType.length > 0"><em>{{accessLog.recordType}} <keyword :v-word="vKeyword">{{accessLog.recordValue}}</keyword></em></span>
<span v-else class="disabled">&nbsp;[没有记录]</span>
<!-- &nbsp; <a href="" @click.prevent="showLog" title="查看详情"><i class="icon expand"></i></a>-->
<div v-if="(accessLog.nsRoutes != null && accessLog.nsRoutes.length > 0) || accessLog.isRecursive" style="margin-top: 0.3em">
<span class="ui label tiny basic grey" v-for="route in accessLog.nsRoutes">线路: {{route.name}}</span>
<span class="ui label tiny basic grey" v-if="accessLog.isRecursive">递归DNS</span>
</div>
<div v-if="accessLog.error != null && accessLog.error.length > 0" style="color:#dc143c">
<i class="icon warning circle"></i>错误:[{{accessLog.error}}]
</div>
</div>`
})

View File

@@ -0,0 +1,41 @@
Vue.component("ns-access-log-ref-box", {
props: ["v-access-log-ref", "v-is-parent"],
data: function () {
let config = this.vAccessLogRef
if (config == null) {
config = {
isOn: false,
isPrior: false,
logMissingDomains: false
}
}
if (typeof (config.logMissingDomains) == "undefined") {
config.logMissingDomains = false
}
return {
config: config
}
},
template: `<div>
<input type="hidden" name="accessLogJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">
<prior-checkbox :v-config="config" v-if="!vIsParent"></prior-checkbox>
<tbody v-show="vIsParent || config.isPrior">
<tr>
<td class="title">启用</td>
<td>
<checkbox name="isOn" value="1" v-model="config.isOn"></checkbox>
</td>
</tr>
<tr>
<td>记录所有访问</td>
<td>
<checkbox name="logMissingDomains" value="1" v-model="config.logMissingDomains"></checkbox>
<p class="comment">包括对没有在系统里创建的域名访问。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,112 @@
Vue.component("ns-create-records-table", {
props: ["v-types", "v-default-ttl"],
data: function () {
let types = this.vTypes
if (types == null) {
types = []
}
let defaultTTL = this.vDefaultTtl
if (defaultTTL != null) {
defaultTTL = parseInt(defaultTTL.toString())
}
if (defaultTTL <= 0) {
defaultTTL = 600
}
return {
types: types,
defaultTTL: defaultTTL,
records: [
{
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: defaultTTL,
index: 0
}
],
lastIndex: 0,
isAddingRoutes: false // 是否正在添加线路
}
},
methods: {
add: function () {
this.records.push({
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: this.defaultTTL,
index: ++this.lastIndex
})
let that = this
setTimeout(function () {
that.$refs.nameInputs.$last().focus()
}, 100)
},
remove: function (index) {
this.records.$remove(index)
},
addRoutes: function () {
this.isAddingRoutes = true
},
cancelRoutes: function () {
let that = this
setTimeout(function () {
that.isAddingRoutes = false
}, 1000)
},
changeRoutes: function (record, routes) {
if (routes == null) {
record.routeCodes = []
} else {
record.routeCodes = routes.map(function (route) {
return route.code
})
}
}
},
template: `<div>
<input type="hidden" name="recordsJSON" :value="JSON.stringify(records)"/>
<table class="ui table selectable celled" style="max-width: 60em">
<thead class="full-width">
<tr>
<th style="width:10em">记录名</th>
<th style="width:7em">记录类型</th>
<th>线路</th>
<th v-if="!isAddingRoutes">记录值</th>
<th v-if="!isAddingRoutes">TTL</th>
<th class="one op" v-if="!isAddingRoutes">操作</th>
</tr>
</thead>
<tr v-for="(record, index) in records" :key="record.index">
<td>
<input type="text" style="width:10em" v-model="record.name" ref="nameInputs"/>
</td>
<td>
<select class="ui dropdown auto-width" v-model="record.type">
<option v-for="type in types" :value="type.type">{{type.type}}</option>
</select>
</td>
<td>
<ns-routes-selector @add="addRoutes" @cancel="cancelRoutes" @change="changeRoutes(record, $event)"></ns-routes-selector>
</td>
<td v-if="!isAddingRoutes">
<input type="text" style="width:10em" maxlength="512" v-model="record.value"/>
</td>
<td v-if="!isAddingRoutes">
<div class="ui input right labeled">
<input type="text" v-model="record.ttl" style="width:5em" maxlength="8"/>
<span class="ui label">秒</span>
</div>
</td>
<td v-if="!isAddingRoutes">
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
</div>`,
})

View File

@@ -0,0 +1,39 @@
Vue.component("ns-domain-group-selector", {
props: ["v-domain-group-id"],
data: function () {
let groupId = this.vDomainGroupId
if (groupId == null) {
groupId = 0
}
return {
userId: 0,
groupId: groupId
}
},
methods: {
change: function (group) {
if (group != null) {
this.$emit("change", group.id)
} else {
this.$emit("change", 0)
}
},
reload: function (userId) {
this.userId = userId
this.$refs.comboBox.clear()
this.$refs.comboBox.setDataURL("/ns/domains/groups/options?userId=" + userId)
this.$refs.comboBox.reloadData()
}
},
template: `<div>
<combo-box
data-url="/ns/domains/groups/options"
placeholder="选择分组"
data-key="groups"
name="groupId"
:v-value="groupId"
@change="change"
ref="comboBox">
</combo-box>
</div>`
})

View File

@@ -0,0 +1,154 @@
Vue.component("ns-record-health-check-config-box", {
props:["value", "v-parent-config"],
data: function () {
let config = this.value
if (config == null) {
config = {
isOn: false,
port: 0,
timeoutSeconds: 0,
countUp: 0,
countDown: 0
}
}
let parentConfig = this.vParentConfig
return {
config: config,
portString: config.port.toString(),
timeoutSecondsString: config.timeoutSeconds.toString(),
countUpString: config.countUp.toString(),
countDownString: config.countDown.toString(),
portIsEditing: config.port > 0,
timeoutSecondsIsEditing: config.timeoutSeconds > 0,
countUpIsEditing: config.countUp > 0,
countDownIsEditing: config.countDown > 0,
parentConfig: parentConfig
}
},
watch: {
portString: function (value) {
let port = parseInt(value.toString())
if (isNaN(port) || port > 65535 || port < 1) {
this.config.port = 0
} else {
this.config.port = port
}
},
timeoutSecondsString: function (value) {
let timeoutSeconds = parseInt(value.toString())
if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) {
this.config.timeoutSeconds = 0
} else {
this.config.timeoutSeconds = timeoutSeconds
}
},
countUpString: function (value) {
let countUp = parseInt(value.toString())
if (isNaN(countUp) || countUp > 1000 || countUp < 1) {
this.config.countUp = 0
} else {
this.config.countUp = countUp
}
},
countDownString: function (value) {
let countDown = parseInt(value.toString())
if (isNaN(countDown) || countDown > 1000 || countDown < 1) {
this.config.countDown = 0
} else {
this.config.countDown = countDown
}
}
},
template: `<div>
<input type="hidden" name="recordHealthCheckJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">
<tbody>
<tr>
<td class="title">启用当前记录健康检查</td>
<td>
<checkbox v-model="config.isOn"></checkbox>
</td>
</tr>
</tbody>
<tbody v-show="config.isOn">
<tr>
<td>检测端口</td>
<td>
<span v-if="!portIsEditing" class="grey">
默认{{parentConfig.port}}
&nbsp; <a href="" @click.prevent="portIsEditing = true; portString = parentConfig.port">[修改]</a>
</span>
<div v-show="portIsEditing">
<div style="margin-bottom: 0.5em">
<a href="" @click.prevent="portIsEditing = false; portString = '0'">[使用默认]</a>
</div>
<input type="text" v-model="portString" maxlength="5" style="width: 5em"/>
<p class="comment">通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。</p>
</div>
</td>
</tr>
<tr>
<td>超时时间</td>
<td>
<span v-if="!timeoutSecondsIsEditing" class="grey">
默认{{parentConfig.timeoutSeconds}}秒
&nbsp; <a href="" @click.prevent="timeoutSecondsIsEditing = true; timeoutSecondsString = parentConfig.timeoutSeconds">[修改]</a>
</span>
<div v-show="timeoutSecondsIsEditing">
<div style="margin-bottom: 0.5em">
<a href="" @click.prevent="timeoutSecondsIsEditing = false; timeoutSecondsString = '0'">[使用默认]</a>
</div>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="timeoutSecondsString" maxlength="3"/>
<span class="ui label">秒</span>
</div>
</div>
</td>
</tr>
<tr>
<td>默认连续上线次数</td>
<td>
<span v-if="!countUpIsEditing" class="grey">
默认{{parentConfig.countUp}}次
&nbsp; <a href="" @click.prevent="countUpIsEditing = true; countUpString = parentConfig.countUp">[修改]</a>
</span>
<div v-show="countUpIsEditing">
<div style="margin-bottom: 0.5em">
<a href="" @click.prevent="countUpIsEditing = false; countUpString = '0'">[使用默认]</a>
</div>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="countUpString" maxlength="3"/>
<span class="ui label">次</span>
</div>
<p class="comment">连续检测<span v-if="config.countUp > 0">{{config.countUp}}</span><span v-else>N</span>次成功后,认为当前记录是在线的。</p>
</div>
</td>
</tr>
<tr>
<td>默认连续下线次数</td>
<td>
<span v-if="!countDownIsEditing" class="grey">
默认{{parentConfig.countDown}}次
&nbsp; <a href="" @click.prevent="countDownIsEditing = true; countDownString = parentConfig.countDown">[修改]</a>
</span>
<div v-show="countDownIsEditing">
<div style="margin-bottom: 0.5em">
<a href="" @click.prevent="countDownIsEditing = false; countDownString = '0'">[使用默认]</a>
</div>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="countDownString" maxlength="3"/>
<span class="ui label">次</span>
</div>
<p class="comment">连续检测<span v-if="config.countDown > 0">{{config.countDown}}</span><span v-else>N</span>次失败后,认为当前记录是离线的。</p>
</div>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,109 @@
Vue.component("ns-records-health-check-config-box", {
props:["value"],
data: function () {
let config = this.value
if (config == null) {
config = {
isOn: false,
port: 80,
timeoutSeconds: 5,
countUp: 1,
countDown: 3
}
}
return {
config: config,
portString: config.port.toString(),
timeoutSecondsString: config.timeoutSeconds.toString(),
countUpString: config.countUp.toString(),
countDownString: config.countDown.toString()
}
},
watch: {
portString: function (value) {
let port = parseInt(value.toString())
if (isNaN(port) || port > 65535 || port < 1) {
this.config.port = 80
} else {
this.config.port = port
}
},
timeoutSecondsString: function (value) {
let timeoutSeconds = parseInt(value.toString())
if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) {
this.config.timeoutSeconds = 5
} else {
this.config.timeoutSeconds = timeoutSeconds
}
},
countUpString: function (value) {
let countUp = parseInt(value.toString())
if (isNaN(countUp) || countUp > 1000 || countUp < 1) {
this.config.countUp = 1
} else {
this.config.countUp = countUp
}
},
countDownString: function (value) {
let countDown = parseInt(value.toString())
if (isNaN(countDown) || countDown > 1000 || countDown < 1) {
this.config.countDown = 3
} else {
this.config.countDown = countDown
}
}
},
template: `<div>
<input type="hidden" name="recordsHealthCheckJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">
<tbody>
<tr>
<td class="title">启用健康检查</td>
<td>
<checkbox v-model="config.isOn"></checkbox>
<p class="comment">选中后表示启用当前域名下A/AAAA记录的健康检查启用此设置后你仍需设置单个A/AAAA记录的健康检查。</p>
</td>
</tr>
</tbody>
<tbody v-show="config.isOn">
<tr>
<td>默认检测端口</td>
<td>
<input type="text" v-model="portString" maxlength="5" style="width: 5em"/>
<p class="comment">通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。</p>
</td>
</tr>
<tr>
<td>默认超时时间</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="timeoutSecondsString" maxlength="3"/>
<span class="ui label">秒</span>
</div>
</td>
</tr>
<tr>
<td>默认连续上线次数</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="countUpString" maxlength="3"/>
<span class="ui label">次</span>
</div>
<p class="comment">连续检测<span v-if="config.countUp > 0">{{config.countUp}}</span><span v-else>N</span>次成功后,认为当前记录是在线的。</p>
</td>
</tr>
<tr>
<td>默认连续下线次数</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 4em" v-model="countDownString" maxlength="3"/>
<span class="ui label">次</span>
</div>
<p class="comment">连续检测<span v-if="config.countDown > 0">{{config.countDown}}</span><span v-else>N</span>次失败后,认为当前记录是离线的。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,165 @@
// 递归DNS设置
Vue.component("ns-recursion-config-box", {
props: ["v-recursion-config"],
data: function () {
let recursion = this.vRecursionConfig
if (recursion == null) {
recursion = {
isOn: false,
hosts: [],
allowDomains: [],
denyDomains: [],
useLocalHosts: false
}
}
if (recursion.hosts == null) {
recursion.hosts = []
}
if (recursion.allowDomains == null) {
recursion.allowDomains = []
}
if (recursion.denyDomains == null) {
recursion.denyDomains = []
}
return {
config: recursion,
hostIsAdding: false,
host: "",
updatingHost: null
}
},
methods: {
changeHosts: function (hosts) {
this.config.hosts = hosts
},
changeAllowDomains: function (domains) {
this.config.allowDomains = domains
},
changeDenyDomains: function (domains) {
this.config.denyDomains = domains
},
removeHost: function (index) {
this.config.hosts.$remove(index)
},
addHost: function () {
this.updatingHost = null
this.host = ""
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
updateHost: function (host) {
this.updatingHost = host
this.host = host.host
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
confirmHost: function () {
if (this.host.length == 0) {
teaweb.warn("请输入DNS地址")
return
}
// TODO 校验Host
// TODO 可以输入端口号
// TODO 可以选择协议
this.hostIsAdding = false
if (this.updatingHost == null) {
this.config.hosts.push({
host: this.host
})
} else {
this.updatingHost.host = this.host
}
},
cancelHost: function () {
this.hostIsAdding = false
}
},
template: `<div>
<input type="hidden" name="recursionJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">
<tbody>
<tr>
<td class="title">启用</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="config.isOn"/>
<label></label>
</div>
<p class="comment">启用后如果找不到某个域名的解析记录则向上一级DNS查找。</p>
</td>
</tr>
</tbody>
<tbody v-show="config.isOn">
<tr>
<td>从节点本机读取<br/>上级DNS主机</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="useLocalHosts" value="1" v-model="config.useLocalHosts"/>
<label></label>
</div>
<p class="comment">选中后,节点会试图从<code-label>/etc/resolv.conf</code-label>文件中读取DNS配置。 </p>
</td>
</tr>
<tr v-show="!config.useLocalHosts">
<td>上级DNS主机地址 *</td>
<td>
<div v-if="config.hosts.length > 0">
<div v-for="(host, index) in config.hosts" class="ui label tiny basic">
{{host.host}} &nbsp;
<a href="" title="修改" @click.prevent="updateHost(host)"><i class="icon pencil tiny"></i></a>
<a href="" title="删除" @click.prevent="removeHost(index)"><i class="icon remove small"></i></a>
</div>
<div class="ui divider"></div>
</div>
<div v-if="hostIsAdding">
<div class="ui fields inline">
<div class="ui field">
<input type="text" placeholder="DNS主机地址" v-model="host" ref="hostRef" @keyup.enter="confirmHost" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirmHost">确认</button> &nbsp; <a href="" title="取消" @click.prevent="cancelHost"><i class="icon remove small"></i></a>
</div>
</div>
</div>
<div style="margin-top: 0.5em">
<button type="button" class="ui button tiny" @click.prevent="addHost">+</button>
</div>
</td>
</tr>
<tr>
<td>允许的域名</td>
<td><values-box name="allowDomains" :values="config.allowDomains" @change="changeAllowDomains"></values-box>
<p class="comment">支持星号通配符,比如<code-label>*.example.org</code-label>。</p>
</td>
</tr>
<tr>
<td>不允许的域名</td>
<td>
<values-box name="denyDomains" :values="config.denyDomains" @change="changeDenyDomains"></values-box>
<p class="comment">支持星号通配符,比如<code-label>*.example.org</code-label>。优先级比允许的域名高。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,585 @@
Vue.component("ns-route-ranges-box", {
props: ["v-ranges"],
data: function () {
let ranges = this.vRanges
if (ranges == null) {
ranges = []
}
return {
ranges: ranges,
isAdding: false,
isAddingBatch: false,
// 类型
rangeType: "ipRange",
isReverse: false,
// IP范围
ipRangeFrom: "",
ipRangeTo: "",
batchIPRange: "",
// CIDR
ipCIDR: "",
batchIPCIDR: "",
// region
regions: [],
regionType: "country",
regionConnector: "OR"
}
},
methods: {
addIPRange: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipRangeFrom.focus()
}, 100)
},
addCIDR: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipCIDR.focus()
}, 100)
},
addRegions: function () {
this.isAdding = true
},
addRegion: function (regionType) {
this.regionType = regionType
},
remove: function (index) {
this.ranges.$remove(index)
},
cancelIPRange: function () {
this.isAdding = false
this.ipRangeFrom = ""
this.ipRangeTo = ""
this.isReverse = false
},
cancelIPCIDR: function () {
this.isAdding = false
this.ipCIDR = ""
this.isReverse = false
},
cancelRegions: function () {
this.isAdding = false
this.regions = []
this.regionType = "country"
this.regionConnector = "OR"
this.isReverse = false
},
confirmIPRange: function () {
// 校验IP
let that = this
this.ipRangeFrom = this.ipRangeFrom.trim()
if (!this.validateIP(this.ipRangeFrom)) {
teaweb.warn("开始IP填写错误", function () {
that.$refs.ipRangeFrom.focus()
})
return
}
this.ipRangeTo = this.ipRangeTo.trim()
if (!this.validateIP(this.ipRangeTo)) {
teaweb.warn("结束IP填写错误", function () {
that.$refs.ipRangeTo.focus()
})
return
}
this.ranges.push({
type: "ipRange",
params: {
ipFrom: this.ipRangeFrom,
ipTo: this.ipRangeTo,
isReverse: this.isReverse
}
})
this.cancelIPRange()
},
confirmIPCIDR: function () {
let that = this
if (this.ipCIDR.length == 0) {
teaweb.warn("请填写CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
if (!this.validateCIDR(this.ipCIDR)) {
teaweb.warn("请输入正确的CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
this.ranges.push({
type: "cidr",
params: {
cidr: this.ipCIDR,
isReverse: this.isReverse
}
})
this.cancelIPCIDR()
},
confirmRegions: function () {
if (this.regions.length == 0) {
this.cancelRegions()
return
}
this.ranges.push({
type: "region",
connector: this.regionConnector,
params: {
regions: this.regions,
isReverse: this.isReverse
}
})
this.cancelRegions()
},
addBatchIPRange: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPRange.focus()
}, 100)
},
addBatchCIDR: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPCIDR.focus()
}, 100)
},
cancelBatchIPRange: function () {
this.isAddingBatch = false
this.batchIPRange = ""
this.isReverse = false
},
cancelBatchIPCIDR: function () {
this.isAddingBatch = false
this.batchIPCIDR = ""
this.isReverse = false
},
confirmBatchIPRange: function () {
let that = this
let rangesText = this.batchIPRange
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的IP范围", function () {
that.$refs.batchIPRange.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
line = line.trim()
if (line.length == 0) {
return
}
line = line.replace("", ",")
let pieces = line.split(",")
if (pieces.length != 2) {
invalidLine = line
return
}
let ipFrom = pieces[0].trim()
let ipTo = pieces[1].trim()
if (!that.validateIP(ipFrom) || !that.validateIP(ipTo)) {
invalidLine = line
return
}
validRanges.push({
type: "ipRange",
params: {
ipFrom: ipFrom,
ipTo: ipTo,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPRange.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPRange()
},
confirmBatchIPCIDR: function () {
let that = this
let rangesText = this.batchIPCIDR
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的CIDR", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
let cidr = line.trim()
if (cidr.length == 0) {
return
}
if (!that.validateCIDR(cidr)) {
invalidLine = line
return
}
validRanges.push({
type: "cidr",
params: {
cidr: cidr,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPCIDR()
},
selectRegionCountry: function (country) {
if (country == null) {
return
}
this.regions.push({
type: "country",
id: country.id,
name: country.name
})
this.$refs.regionCountryComboBox.clear()
},
selectRegionProvince: function (province) {
if (province == null) {
return
}
this.regions.push({
type: "province",
id: province.id,
name: province.name
})
this.$refs.regionProvinceComboBox.clear()
},
selectRegionCity: function (city) {
if (city == null) {
return
}
this.regions.push({
type: "city",
id: city.id,
name: city.name
})
this.$refs.regionCityComboBox.clear()
},
selectRegionProvider: function (provider) {
if (provider == null) {
return
}
this.regions.push({
type: "provider",
id: provider.id,
name: provider.name
})
this.$refs.regionProviderComboBox.clear()
},
removeRegion: function (index) {
this.regions.$remove(index)
},
validateIP: function (ip) {
if (ip.length == 0) {
return
}
// IPv6
if (ip.indexOf(":") >= 0) {
let pieces = ip.split(":")
if (pieces.length > 8) {
return false
}
let isOk = true
pieces.forEach(function (piece) {
if (!/^[\da-fA-F]{0,4}$/.test(piece)) {
isOk = false
}
})
return isOk
}
if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
return false
}
let pieces = ip.split(".")
let isOk = true
pieces.forEach(function (v) {
let v1 = parseInt(v)
if (v1 > 255) {
isOk = false
}
})
return isOk
},
validateCIDR: function (cidr) {
let pieces = cidr.split("/")
if (pieces.length != 2) {
return false
}
let ip = pieces[0]
if (!this.validateIP(ip)) {
return false
}
let mask = pieces[1]
if (!/^\d{1,3}$/.test(mask)) {
return false
}
mask = parseInt(mask, 10)
if (cidr.indexOf(":") >= 0) { // IPv6
return mask <= 128
}
return mask <= 32
},
updateRangeType: function (rangeType) {
this.rangeType = rangeType
}
},
template: `<div>
<input type="hidden" name="rangesJSON" :value="JSON.stringify(ranges)"/>
<div v-if="ranges.length > 0">
<div class="ui label tiny basic" v-for="(range, index) in ranges" style="margin-bottom: 0.3em">
<span class="red" v-if="range.params.isReverse">[排除]</span>
<span v-if="range.type == 'ipRange'">IP范围</span>
<span v-if="range.type == 'cidr'">CIDR</span>
<span v-if="range.type == 'region'"></span>
<span v-if="range.type == 'ipRange'">{{range.params.ipFrom}} - {{range.params.ipTo}}</span>
<span v-if="range.type == 'cidr'">{{range.params.cidr}}</span>
<span v-if="range.type == 'region'">
<span v-for="(region, index) in range.params.regions">
<span v-if="region.type == 'country'">国家/地区</span>
<span v-if="region.type == 'province'">省份</span>
<span v-if="region.type == 'city'">城市</span>
<span v-if="region.type == 'provider'">ISP</span>
{{region.name}}
<span v-if="index < range.params.regions.length - 1" class="grey">
&nbsp;
<span v-if="range.connector == 'OR' || range.connector == '' || range.connector == null">或</span>
<span v-if="range.connector == 'AND'">且</span>
&nbsp;
</span>
</span>
</span>
&nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</div>
<div class="ui divider"></div>
</div>
<!-- IP范围 -->
<div v-if="rangeType == 'ipRange'">
<!-- 添加单个IP范围 -->
<div style="margin-bottom: 1em" v-show="isAdding">
<table class="ui table">
<tr>
<td class="title">开始IP *</td>
<td>
<input type="text" placeholder="开始IP" maxlength="40" size="40" style="width: 15em" v-model="ipRangeFrom" ref="ipRangeFrom" @keyup.enter="confirmIPRange" @keypress.enter.prevent="1"/>
</td>
</tr>
<tr>
<td>结束IP *</td>
<td>
<input type="text" placeholder="结束IP" maxlength="40" size="40" style="width: 15em" v-model="ipRangeTo" ref="ipRangeTo" @keyup.enter="confirmIPRange" @keypress.enter.prevent="1"/>
</td>
</tr>
<tr>
<td>排除</td>
<td>
<checkbox v-model="isReverse"></checkbox>
<p class="comment">选中后表示线路中排除当前条件。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="confirmIPRange">确定</button> &nbsp;
<a href="" @click.prevent="cancelIPRange" title="取消"><i class="icon remove small"></i></a>
</div>
<!-- 添加多个IP范围 -->
<div style="margin-bottom: 1em" v-show="isAddingBatch">
<table class="ui table">
<tr>
<td class="title">IP范围列表 *</td>
<td>
<textarea rows="5" ref="batchIPRange" v-model="batchIPRange"></textarea>
<p class="comment">每行一条,格式为<code-label>开始IP,结束IP</code-label>,比如<code-label>192.168.1.100,192.168.1.200</code-label>。</p>
</td>
</tr>
<tr>
<td>排除</td>
<td>
<checkbox v-model="isReverse"></checkbox>
<p class="comment">选中后表示线路中排除当前条件。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="confirmBatchIPRange">确定</button> &nbsp;
<a href="" @click.prevent="cancelBatchIPRange" title="取消"><i class="icon remove small"></i></a>
</div>
<div v-if="!isAdding && !isAddingBatch">
<button class="ui button tiny" type="button" @click.prevent="addIPRange">添加单个IP范围</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="addBatchIPRange">批量添加IP范围</button>
</div>
</div>
<!-- CIDR -->
<div v-if="rangeType == 'cidr'">
<!-- 添加单个IP范围 -->
<div style="margin-bottom: 1em" v-show="isAdding">
<table class="ui table">
<tr>
<td class="title">CIDR *</td>
<td>
<input type="text" placeholder="IP/MASK" maxlength="40" size="40" style="width: 15em" v-model="ipCIDR" ref="ipCIDR" @keyup.enter="confirmIPCIDR" @keypress.enter.prevent="1"/>
<p class="comment">类似于<code-label>192.168.2.1/24</code-label>。</p>
</td>
</tr>
<tr>
<td>排除</td>
<td>
<checkbox v-model="isReverse"></checkbox>
<p class="comment">选中后表示线路中排除当前条件。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="confirmIPCIDR">确定</button> &nbsp;
<a href="" @click.prevent="cancelIPCIDR" title="取消"><i class="icon remove small"></i></a>
</div>
<!-- 添加多个IP范围 -->
<div style="margin-bottom: 1em" v-show="isAddingBatch">
<table class="ui table">
<tr>
<td class="title">IP范围列表 *</td>
<td>
<textarea rows="5" ref="batchIPCIDR" v-model="batchIPCIDR"></textarea>
<p class="comment">每行一条,格式为<code-label>IP/MASK</code-label>,比如<code-label>192.168.2.1/24</code-label>。</p>
</td>
</tr>
<tr>
<td>排除</td>
<td>
<checkbox v-model="isReverse"></checkbox>
<p class="comment">选中后表示线路中排除当前条件。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="confirmBatchIPCIDR">确定</button> &nbsp;
<a href="" @click.prevent="cancelBatchIPCIDR" title="取消"><i class="icon remove small"></i></a>
</div>
<div v-if="!isAdding && !isAddingBatch">
<button class="ui button tiny" type="button" @click.prevent="addCIDR">添加单个CIDR</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="addBatchCIDR">批量添加CIDR</button>
</div>
</div>
<!-- 区域 -->
<div v-if="rangeType == 'region'">
<!-- 添加区域 -->
<div v-if="isAdding">
<table class="ui table">
<tr>
<td>已添加</td>
<td>
<span v-for="(region, index) in regions">
<span class="ui label small basic">
<span v-if="region.type == 'country'">国家/地区</span>
<span v-if="region.type == 'province'">省份</span>
<span v-if="region.type == 'city'">城市</span>
<span v-if="region.type == 'provider'">ISP</span>
{{region.name}} <a href="" title="删除" @click.prevent="removeRegion(index)"><i class="icon remove small"></i></a>
</span>
<span v-if="index < regions.length - 1" class="grey">
&nbsp;
<span v-if="regionConnector == 'OR' || regionConnector == ''">或</span>
<span v-if="regionConnector == 'AND'">且</span>
&nbsp;
</span>
</span>
</td>
</tr>
<tr>
<td class="title">添加新<span v-if="regionType == 'country'">国家/地区</span><span v-if="regionType == 'province'">省份</span><span v-if="regionType == 'city'">城市</span><span v-if="regionType == 'provider'">ISP</span>
*</td>
<td>
<!-- region country name -->
<div v-if="regionType == 'country'">
<combo-box title="" width="14em" data-url="/ui/countryOptions" data-key="countries" placeholder="点这里选择国家/地区" @change="selectRegionCountry" ref="regionCountryComboBox" key="combo-box-country"></combo-box>
</div>
<!-- region province name -->
<div v-if="regionType == 'province'" >
<combo-box title="" data-url="/ui/provinceOptions" data-key="provinces" placeholder="点这里选择省份" @change="selectRegionProvince" ref="regionProvinceComboBox" key="combo-box-province"></combo-box>
</div>
<!-- region city name -->
<div v-if="regionType == 'city'" >
<combo-box title="" data-url="/ui/cityOptions" data-key="cities" placeholder="点这里选择城市" @change="selectRegionCity" ref="regionCityComboBox" key="combo-box-city"></combo-box>
</div>
<!-- ISP Name -->
<div v-if="regionType == 'provider'" >
<combo-box title="" data-url="/ui/providerOptions" data-key="providers" placeholder="点这里选择ISP" @change="selectRegionProvider" ref="regionProviderComboBox" key="combo-box-isp"></combo-box>
</div>
<div style="margin-top: 1em">
<button class="ui button tiny basic" :class="{blue: regionType == 'country'}" type="button" @click.prevent="addRegion('country')">添加国家/地区</button> &nbsp;
<button class="ui button tiny basic" :class="{blue: regionType == 'province'}" type="button" @click.prevent="addRegion('province')">添加省份</button> &nbsp;
<button class="ui button tiny basic" :class="{blue: regionType == 'city'}" type="button" @click.prevent="addRegion('city')">添加城市</button> &nbsp;
<button class="ui button tiny basic" :class="{blue: regionType == 'provider'}" type="button" @click.prevent="addRegion('provider')">ISP</button> &nbsp;
</div>
</td>
</tr>
<tr>
<td>区域之间关系</td>
<td>
<select class="ui dropdown auto-width" v-model="regionConnector">
<option value="OR">或</option>
<option value="AND">且</option>
</select>
<p class="comment" v-if="regionConnector == 'OR'">匹配所选任一区域即认为匹配成功。</p>
<p class="comment" v-if="regionConnector == 'AND'">匹配所有所选区域才认为匹配成功。</p>
</td>
</tr>
<tr>
<td>排除</td>
<td>
<checkbox v-model="isReverse"></checkbox>
<p class="comment">选中后表示线路中排除当前条件。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="confirmRegions">确定</button> &nbsp;
<a href="" @click.prevent="cancelRegions" title="取消"><i class="icon remove small"></i></a>
</div>
<div v-if="!isAdding && !isAddingBatch">
<button class="ui button tiny" type="button" @click.prevent="addRegions">添加区域</button> &nbsp;
</div>
</div>
</div>`
})

View File

@@ -0,0 +1,30 @@
// 选择单一线路
Vue.component("ns-route-selector", {
props: ["v-route-code"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
})
},
data: function () {
let routeCode = this.vRouteCode
if (routeCode == null) {
routeCode = ""
}
return {
routeCode: routeCode,
routes: []
}
},
template: `<div>
<div v-if="routes.length > 0">
<select class="ui dropdown" name="routeCode" v-model="routeCode">
<option value="">[线路]</option>
<option v-for="route in routes" :value="route.code">{{route.name}}</option>
</select>
</div>
</div>`
})

View File

@@ -0,0 +1,184 @@
// 选择多个线路
Vue.component("ns-routes-selector", {
props: ["v-routes", "name"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes
that.supportISPRoutes = resp.data.supportISPRoutes
that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes
that.supportAgentRoutes = resp.data.supportAgentRoutes
that.publicCategories = resp.data.publicCategories
that.supportPublicRoutes = resp.data.supportPublicRoutes
// provinces
let provinces = {}
if (resp.data.provinces != null && resp.data.provinces.length > 0) {
for (const province of resp.data.provinces) {
let countryCode = province.countryCode
if (typeof provinces[countryCode] == "undefined") {
provinces[countryCode] = []
}
provinces[countryCode].push({
name: province.name,
code: province.code
})
}
}
that.provinces = provinces
})
},
data: function () {
let selectedRoutes = this.vRoutes
if (selectedRoutes == null) {
selectedRoutes = []
}
let inputName = this.name
if (typeof inputName != "string" || inputName.length == 0) {
inputName = "routeCodes"
}
return {
routeCode: "default",
inputName: inputName,
routes: [],
provinces: {}, // country code => [ province1, province2, ... ]
provinceRouteCode: "",
isAdding: false,
routeType: "default",
selectedRoutes: selectedRoutes,
supportChinaProvinceRoutes: false,
supportISPRoutes: false,
supportWorldRegionRoutes: false,
supportAgentRoutes: false,
supportPublicRoutes: false,
publicCategories: []
}
},
watch: {
routeType: function (v) {
this.routeCode = ""
let that = this
this.routes.forEach(function (route) {
if (route.type == v && that.routeCode.length == 0) {
that.routeCode = route.code
}
})
}
},
methods: {
add: function () {
this.isAdding = true
this.routeType = "default"
this.routeCode = "default"
this.provinceRouteCode = ""
this.$emit("add")
},
cancel: function () {
this.isAdding = false
this.$emit("cancel")
},
confirm: function () {
if (this.routeCode.length == 0) {
return
}
let that = this
// route
let selectedRoute = null
for (const route of this.routes) {
if (route.code == this.routeCode) {
selectedRoute = route
break
}
}
if (selectedRoute != null) {
// province route
if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) {
for (const province of this.provinces[this.routeCode]) {
if (province.code == this.provinceRouteCode) {
selectedRoute = {
name: selectedRoute.name + "-" + province.name,
code: province.code
}
break
}
}
}
that.selectedRoutes.push(selectedRoute)
}
this.$emit("change", this.selectedRoutes)
this.cancel()
},
remove: function (index) {
this.selectedRoutes.$remove(index)
this.$emit("change", this.selectedRoutes)
}
}
,
template: `<div>
<div v-show="selectedRoutes.length > 0">
<div class="ui label basic text small" v-for="(route, index) in selectedRoutes" style="margin-bottom: 0.3em">
<input type="hidden" :name="inputName" :value="route.code"/>
{{route.name}} &nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</div>
<div class="ui divider"></div>
</div>
<div v-if="isAdding" style="margin-bottom: 1em">
<table class="ui table">
<tr>
<td class="title">选择类型 *</td>
<td>
<select class="ui dropdown auto-width" v-model="routeType">
<option value="default">[默认线路]</option>
<option value="user">自定义线路</option>
<optgroup label="---"></optgroup>
<option value="isp" v-if="supportISPRoutes">运营商</option>
<option value="china" v-if="supportChinaProvinceRoutes">中国省市</option>
<option value="world" v-if="supportWorldRegionRoutes">全球国家地区</option>
<option value="agent" v-if="supportAgentRoutes">搜索引擎</option>
<optgroup label="---" v-if="publicCategories.length > 0"></optgroup>
<option v-for="category in publicCategories" :value="category.type">{{category.name}}</option>
</select>
</td>
</tr>
<tr>
<td>选择线路 *</td>
<td>
<select class="ui dropdown auto-width" v-model="routeCode">
<option v-for="route in routes" :value="route.code" v-if="route.type == routeType">{{route.name}}</option>
</select>
</td>
</tr>
<tr v-if="routeCode.length > 0 && provinces[routeCode] != null">
<td>选择省/州</td>
<td>
<select class="ui dropdown auto-width" v-model="provinceRouteCode">
<option value="">[全域]</option>
<option v-for="province in provinces[routeCode]" :value="province.code">{{province.name}}</option>
</select>
</td>
</tr>
</table>
<div>
<button type="button" class="ui button tiny" @click.prevent="confirm">确定</button>
&nbsp; <a href="" title="取消" @click.prevent="cancel">取消</a>
</div>
</div>
<button class="ui button tiny" type="button" @click.prevent="add" v-if="!isAdding">+</button>
<p class="comment" v-if="!supportISPRoutes || !supportChinaProvinceRoutes || !supportWorldRegionRoutes || !supportPublicRoutes">由于套餐限制,当前用户只能使用部分线路。</p>
</div>`
})