1.4.5.2
This commit is contained in:
3
EdgeAdmin/web/public/js/axios.min.js
vendored
Normal file
3
EdgeAdmin/web/public/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
EdgeAdmin/web/public/js/axios.min.map
Normal file
1
EdgeAdmin/web/public/js/axios.min.map
Normal file
File diff suppressed because one or more lines are too long
7
EdgeAdmin/web/public/js/clipboard.min.js
vendored
Normal file
7
EdgeAdmin/web/public/js/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
23126
EdgeAdmin/web/public/js/components.js
Normal file
23126
EdgeAdmin/web/public/js/components.js
Normal file
File diff suppressed because one or more lines are too long
23126
EdgeAdmin/web/public/js/components.src.js
Normal file
23126
EdgeAdmin/web/public/js/components.src.js
Normal file
File diff suppressed because one or more lines are too long
29
EdgeAdmin/web/public/js/components/admins/admin-selector.js
Normal file
29
EdgeAdmin/web/public/js/components/admins/admin-selector.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// TODO 支持关键词搜索
|
||||
// TODO 改成弹窗选择
|
||||
Vue.component("admin-selector", {
|
||||
props: ["v-admin-id"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/admins/options")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.admins = resp.data.admins
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let adminId = this.vAdminId
|
||||
if (adminId == null) {
|
||||
adminId = 0
|
||||
}
|
||||
return {
|
||||
admins: [],
|
||||
adminId: adminId
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<select class="ui dropdown auto-width" name="adminId" v-model="adminId">
|
||||
<option value="0">[选择系统用户]</option>
|
||||
<option v-for="admin in admins" :value="admin.id">{{admin.name}}({{admin.username}})</option>
|
||||
</select>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,176 @@
|
||||
Vue.component("ad-instance-objects-box", {
|
||||
props: ["v-objects", "v-user-id"],
|
||||
mounted: function () {
|
||||
this.getUserServers(1)
|
||||
},
|
||||
data: function () {
|
||||
let objects = this.vObjects
|
||||
if (objects == null) {
|
||||
objects = []
|
||||
}
|
||||
|
||||
let objectCodes = []
|
||||
objects.forEach(function (v) {
|
||||
objectCodes.push(v.code)
|
||||
})
|
||||
|
||||
return {
|
||||
userId: this.vUserId,
|
||||
objects: objects,
|
||||
objectCodes: objectCodes,
|
||||
isAdding: true,
|
||||
|
||||
servers: [],
|
||||
serversIsLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
},
|
||||
remove: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此防护对象吗?", function () {
|
||||
that.objects.$remove(index)
|
||||
that.notifyChange()
|
||||
})
|
||||
},
|
||||
removeObjectCode: function (objectCode) {
|
||||
let index = -1
|
||||
this.objectCodes.forEach(function (v, k) {
|
||||
if (objectCode == v) {
|
||||
index = k
|
||||
}
|
||||
})
|
||||
if (index >= 0) {
|
||||
this.objects.$remove(index)
|
||||
this.notifyChange()
|
||||
}
|
||||
},
|
||||
getUserServers: function (page) {
|
||||
if (Tea.Vue == null) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.getUserServers(page)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
this.serversIsLoading = true
|
||||
Tea.Vue.$post(".userServers")
|
||||
.params({
|
||||
userId: this.userId,
|
||||
page: page,
|
||||
pageSize: 5
|
||||
})
|
||||
.success(function (resp) {
|
||||
that.servers = resp.data.servers
|
||||
|
||||
that.$refs.serverPage.updateMax(resp.data.page.max)
|
||||
that.serversIsLoading = false
|
||||
})
|
||||
.error(function () {
|
||||
that.serversIsLoading = false
|
||||
})
|
||||
},
|
||||
changeServerPage: function (page) {
|
||||
this.getUserServers(page)
|
||||
},
|
||||
selectServerObject: function (server) {
|
||||
if (this.existObjectCode("server:" + server.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.objects.push({
|
||||
"type": "server",
|
||||
"code": "server:" + server.id,
|
||||
|
||||
"id": server.id,
|
||||
"name": server.name
|
||||
})
|
||||
this.notifyChange()
|
||||
},
|
||||
notifyChange: function () {
|
||||
let objectCodes = []
|
||||
this.objects.forEach(function (v) {
|
||||
objectCodes.push(v.code)
|
||||
})
|
||||
this.objectCodes = objectCodes
|
||||
},
|
||||
existObjectCode: function (objectCode) {
|
||||
let found = false
|
||||
this.objects.forEach(function (v) {
|
||||
if (v.code == objectCode) {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="objectCodesJSON" :value="JSON.stringify(objectCodes)"/>
|
||||
|
||||
<!-- 已有对象 -->
|
||||
<div>
|
||||
<div v-if="objects.length == 0"><span class="grey">暂时还没有设置任何防护对象。</span></div>
|
||||
<div v-if="objects.length > 0">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">已选中防护对象</td>
|
||||
<td>
|
||||
<div v-for="(object, index) in objects" class="ui label basic small" style="margin-bottom: 0.5em">
|
||||
<span v-if="object.type == 'server'">网站:{{object.name}}</span>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
|
||||
<!-- 添加表单 -->
|
||||
<div v-if="isAdding">
|
||||
<table class="ui table celled">
|
||||
<tr>
|
||||
<td class="title">对象类型</td>
|
||||
<td>网站</td>
|
||||
</tr>
|
||||
<!-- 网站列表 -->
|
||||
<tr>
|
||||
<td>网站列表</td>
|
||||
<td>
|
||||
<span v-if="serversIsLoading">加载中...</span>
|
||||
<div v-if="!serversIsLoading && servers.length == 0">暂时还没有可选的网站。</div>
|
||||
<table class="ui table" v-show="!serversIsLoading && servers.length > 0">
|
||||
<thead class="full-width">
|
||||
<tr>
|
||||
<th>网站名称</th>
|
||||
<th class="one op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="server in servers">
|
||||
<td style="background: white">{{server.name}}</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="selectServerObject(server)" v-if="!existObjectCode('server:' + server.id)">选中</a>
|
||||
<a href="" @click.prevent="removeObjectCode('server:' + server.id)" v-else><span class="red">取消</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<js-page ref="serverPage" @change="changeServerPage"></js-page>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<div v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
Vue.component("api-node-addresses-box", {
|
||||
props: ["v-addrs", "v-name"],
|
||||
data: function () {
|
||||
let addrs = this.vAddrs
|
||||
if (addrs == null) {
|
||||
addrs = []
|
||||
}
|
||||
return {
|
||||
addrs: addrs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 添加IP地址
|
||||
addAddr: function () {
|
||||
let that = this;
|
||||
teaweb.popup("/settings/api/node/createAddrPopup", {
|
||||
height: "16em",
|
||||
callback: function (resp) {
|
||||
that.addrs.push(resp.data.addr);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 修改地址
|
||||
updateAddr: function (index, addr) {
|
||||
let that = this;
|
||||
window.UPDATING_ADDR = addr
|
||||
teaweb.popup("/settings/api/node/updateAddrPopup?addressId=", {
|
||||
callback: function (resp) {
|
||||
Vue.set(that.addrs, index, resp.data.addr);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 删除IP地址
|
||||
removeAddr: function (index) {
|
||||
this.addrs.$remove(index);
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(addrs)"/>
|
||||
<div v-if="addrs.length > 0">
|
||||
<div>
|
||||
<div v-for="(addr, index) in addrs" class="ui label small basic">
|
||||
{{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}}</span>
|
||||
<a href="" title="修改" @click.prevent="updateAddr(index, addr)"><i class="icon pencil small"></i></a>
|
||||
<a href="" title="删除" @click.prevent="removeAddr(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button small" type="button" @click.prevent="addAddr()">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
Vue.component("api-node-selector", {
|
||||
props: [],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
template: `<div>
|
||||
暂未实现
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
// 单个集群选择
|
||||
Vue.component("cluster-selector", {
|
||||
props: ["v-cluster-id"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
|
||||
Tea.action("/clusters/options")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.clusters = resp.data.clusters
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let clusterId = this.vClusterId
|
||||
if (clusterId == null) {
|
||||
clusterId = 0
|
||||
}
|
||||
return {
|
||||
clusters: [],
|
||||
clusterId: clusterId
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<select class="ui dropdown" style="max-width: 10em" name="clusterId" v-model="clusterId">
|
||||
<option value="0">[选择集群]</option>
|
||||
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
|
||||
</select>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
Vue.component("ddos-protection-ip-list-config-box", {
|
||||
props: ["v-ip-list"],
|
||||
data: function () {
|
||||
let list = this.vIpList
|
||||
if (list == null) {
|
||||
list = []
|
||||
}
|
||||
return {
|
||||
list: list,
|
||||
isAdding: false,
|
||||
addingIP: {
|
||||
ip: "",
|
||||
description: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingIPInput.focus()
|
||||
})
|
||||
},
|
||||
confirm: function () {
|
||||
let ip = this.addingIP.ip
|
||||
if (ip.length == 0) {
|
||||
this.warn("请输入IP")
|
||||
return
|
||||
}
|
||||
|
||||
let exists = false
|
||||
this.list.forEach(function (v) {
|
||||
if (v.ip == ip) {
|
||||
exists = true
|
||||
}
|
||||
})
|
||||
if (exists) {
|
||||
this.warn("IP '" + ip + "'已经存在")
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
Tea.Vue.$post("/ui/validateIPs")
|
||||
.params({
|
||||
ips: [ip]
|
||||
})
|
||||
.success(function () {
|
||||
that.list.push({
|
||||
ip: ip,
|
||||
description: that.addingIP.description
|
||||
})
|
||||
that.notifyChange()
|
||||
that.cancel()
|
||||
})
|
||||
.fail(function () {
|
||||
that.warn("请输入正确的IP")
|
||||
})
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingIP = {
|
||||
ip: "",
|
||||
description: ""
|
||||
}
|
||||
},
|
||||
remove: function (index) {
|
||||
this.list.$remove(index)
|
||||
this.notifyChange()
|
||||
},
|
||||
warn: function (message) {
|
||||
let that = this
|
||||
teaweb.warn(message, function () {
|
||||
that.$refs.addingIPInput.focus()
|
||||
})
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("change", this.list)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="list.length > 0">
|
||||
<div class="ui label basic tiny" v-for="(ipConfig, index) in list">
|
||||
{{ipConfig.ip}} <span class="grey small" v-if="ipConfig.description.length > 0">({{ipConfig.description}})</span> <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<div class="ui input left labeled">
|
||||
<span class="ui label">IP</span>
|
||||
<input type="text" v-model="addingIP.ip" ref="addingIPInput" maxlength="40" size="20" placeholder="IP" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input left labeled">
|
||||
<span class="ui label">备注</span>
|
||||
<input type="text" v-model="addingIP.description" maxlength="10" size="10" placeholder="备注(可选)" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" @click.prevent="cancel()">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
Vue.component("ddos-protection-ports-config-box", {
|
||||
props: ["v-ports"],
|
||||
data: function () {
|
||||
let ports = this.vPorts
|
||||
if (ports == null) {
|
||||
ports = []
|
||||
}
|
||||
return {
|
||||
ports: ports,
|
||||
isAdding: false,
|
||||
addingPort: {
|
||||
port: "",
|
||||
description: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingPortInput.focus()
|
||||
})
|
||||
},
|
||||
confirm: function () {
|
||||
let portString = this.addingPort.port
|
||||
if (portString.length == 0) {
|
||||
this.warn("请输入端口号")
|
||||
return
|
||||
}
|
||||
if (!/^\d+$/.test(portString)) {
|
||||
this.warn("请输入正确的端口号")
|
||||
return
|
||||
}
|
||||
let port = parseInt(portString, 10)
|
||||
if (port <= 0) {
|
||||
this.warn("请输入正确的端口号")
|
||||
return
|
||||
}
|
||||
if (port > 65535) {
|
||||
this.warn("请输入正确的端口号")
|
||||
return
|
||||
}
|
||||
|
||||
let exists = false
|
||||
this.ports.forEach(function (v) {
|
||||
if (v.port == port) {
|
||||
exists = true
|
||||
}
|
||||
})
|
||||
if (exists) {
|
||||
this.warn("端口号已经存在")
|
||||
return
|
||||
}
|
||||
|
||||
this.ports.push({
|
||||
port: port,
|
||||
description: this.addingPort.description
|
||||
})
|
||||
this.notifyChange()
|
||||
this.cancel()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingPort = {
|
||||
port: "",
|
||||
description: ""
|
||||
}
|
||||
},
|
||||
remove: function (index) {
|
||||
this.ports.$remove(index)
|
||||
this.notifyChange()
|
||||
},
|
||||
warn: function (message) {
|
||||
let that = this
|
||||
teaweb.warn(message, function () {
|
||||
that.$refs.addingPortInput.focus()
|
||||
})
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("change", this.ports)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="ports.length > 0">
|
||||
<div class="ui label basic tiny" v-for="(portConfig, index) in ports">
|
||||
{{portConfig.port}} <span class="grey small" v-if="portConfig.description.length > 0">({{portConfig.description}})</span> <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<div class="ui input left labeled">
|
||||
<span class="ui label">端口</span>
|
||||
<input type="text" v-model="addingPort.port" ref="addingPortInput" maxlength="5" size="5" placeholder="端口号" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input left labeled">
|
||||
<span class="ui label">备注</span>
|
||||
<input type="text" v-model="addingPort.description" maxlength="12" size="12" placeholder="备注(可选)" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" @click.prevent="cancel()">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
Vue.component("node-cluster-combo-box", {
|
||||
props: ["v-cluster-id"],
|
||||
data: function () {
|
||||
let that = this
|
||||
Tea.action("/clusters/options")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.clusters = resp.data.clusters
|
||||
})
|
||||
return {
|
||||
clusters: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function (item) {
|
||||
if (item == null) {
|
||||
this.$emit("change", 0)
|
||||
} else {
|
||||
this.$emit("change", item.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div v-if="clusters.length > 0" style="min-width: 10.4em">
|
||||
<combo-box title="集群" placeholder="集群名称" :v-items="clusters" name="clusterId" :v-value="vClusterId" @change="change"></combo-box>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
// 显示节点的多个集群
|
||||
Vue.component("node-clusters-labels", {
|
||||
props: ["v-primary-cluster", "v-secondary-clusters", "size"],
|
||||
data: function () {
|
||||
let cluster = this.vPrimaryCluster
|
||||
let secondaryClusters = this.vSecondaryClusters
|
||||
if (secondaryClusters == null) {
|
||||
secondaryClusters = []
|
||||
}
|
||||
|
||||
let labelSize = this.size
|
||||
if (labelSize == null) {
|
||||
labelSize = "small"
|
||||
}
|
||||
return {
|
||||
cluster: cluster,
|
||||
secondaryClusters: secondaryClusters,
|
||||
labelSize: labelSize
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<a v-if="cluster != null" :href="'/clusters/cluster?clusterId=' + cluster.id" title="主集群" style="margin-bottom: 0.3em;">
|
||||
<span class="ui label basic grey" :class="labelSize" v-if="labelSize != 'tiny'">{{cluster.name}}</span>
|
||||
<grey-label v-if="labelSize == 'tiny'">{{cluster.name}}</grey-label>
|
||||
</a>
|
||||
<a v-for="c in secondaryClusters" :href="'/clusters/cluster?clusterId=' + c.id" :class="labelSize" title="从集群">
|
||||
<span class="ui label basic grey" :class="labelSize" v-if="labelSize != 'tiny'">{{c.name}}</span>
|
||||
<grey-label v-if="labelSize == 'tiny'">{{c.name}}</grey-label>
|
||||
</a>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
// 一个节点的多个集群选择器
|
||||
Vue.component("node-clusters-selector", {
|
||||
props: ["v-primary-cluster", "v-secondary-clusters"],
|
||||
data: function () {
|
||||
let primaryCluster = this.vPrimaryCluster
|
||||
|
||||
let secondaryClusters = this.vSecondaryClusters
|
||||
if (secondaryClusters == null) {
|
||||
secondaryClusters = []
|
||||
}
|
||||
|
||||
return {
|
||||
primaryClusterId: (primaryCluster == null) ? 0 : primaryCluster.id,
|
||||
secondaryClusterIds: secondaryClusters.map(function (v) {
|
||||
return v.id
|
||||
}),
|
||||
|
||||
primaryCluster: primaryCluster,
|
||||
secondaryClusters: secondaryClusters
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addPrimary: function () {
|
||||
let that = this
|
||||
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
|
||||
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=single", {
|
||||
height: "30em",
|
||||
width: "50em",
|
||||
callback: function (resp) {
|
||||
if (resp.data.cluster != null) {
|
||||
that.primaryCluster = resp.data.cluster
|
||||
that.primaryClusterId = that.primaryCluster.id
|
||||
that.notifyChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
removePrimary: function () {
|
||||
this.primaryClusterId = 0
|
||||
this.primaryCluster = null
|
||||
this.notifyChange()
|
||||
},
|
||||
addSecondary: function () {
|
||||
let that = this
|
||||
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
|
||||
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=multiple", {
|
||||
height: "30em",
|
||||
width: "50em",
|
||||
callback: function (resp) {
|
||||
if (resp.data.cluster != null) {
|
||||
that.secondaryClusterIds.push(resp.data.cluster.id)
|
||||
that.secondaryClusters.push(resp.data.cluster)
|
||||
that.notifyChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
removeSecondary: function (index) {
|
||||
this.secondaryClusterIds.$remove(index)
|
||||
this.secondaryClusters.$remove(index)
|
||||
this.notifyChange()
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("change", {
|
||||
clusterId: this.primaryClusterId
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="primaryClusterId" :value="primaryClusterId"/>
|
||||
<input type="hidden" name="secondaryClusterIds" :value="JSON.stringify(secondaryClusterIds)"/>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">主集群</td>
|
||||
<td>
|
||||
<div v-if="primaryCluster != null">
|
||||
<div class="ui label basic small">{{primaryCluster.name}} <a href="" title="删除" @click.prevent="removePrimary"><i class="icon remove small"></i></a> </div>
|
||||
</div>
|
||||
<div style="margin-top: 0.6em" v-if="primaryClusterId == 0">
|
||||
<button class="ui button tiny" type="button" @click.prevent="addPrimary">+</button>
|
||||
</div>
|
||||
<p class="comment">多个集群配置有冲突时,优先使用主集群配置。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>从集群</td>
|
||||
<td>
|
||||
<div v-if="secondaryClusters.length > 0">
|
||||
<div class="ui label basic small" v-for="(cluster, index) in secondaryClusters"><span class="grey">{{cluster.name}}</span> <a href="" title="删除" @click.prevent="removeSecondary(index)"><i class="icon remove small"></i></a> </div>
|
||||
</div>
|
||||
<div style="margin-top: 0.6em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="addSecondary">+</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
Vue.component("node-ddos-protection-config-box", {
|
||||
props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"],
|
||||
data: function () {
|
||||
let config = this.vDdosProtectionConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
tcp: {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
maxConnections: 0,
|
||||
maxConnectionsPerIP: 0,
|
||||
newConnectionsRate: 0,
|
||||
newConnectionsRateBlockTimeout: 0,
|
||||
newConnectionsSecondlyRate: 0,
|
||||
newConnectionSecondlyRateBlockTimeout: 0,
|
||||
allowIPList: [],
|
||||
ports: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize
|
||||
if (config.tcp == null) {
|
||||
config.tcp = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
maxConnections: 0,
|
||||
maxConnectionsPerIP: 0,
|
||||
newConnectionsRate: 0,
|
||||
newConnectionsRateBlockTimeout: 0,
|
||||
newConnectionsSecondlyRate: 0,
|
||||
newConnectionSecondlyRateBlockTimeout: 0,
|
||||
allowIPList: [],
|
||||
ports: []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
config: config,
|
||||
defaultConfigs: this.vDefaultConfigs,
|
||||
isNode: this.vIsNode,
|
||||
|
||||
isAddingPort: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeTCPPorts: function (ports) {
|
||||
this.config.tcp.ports = ports
|
||||
},
|
||||
changeTCPAllowIPList: function (ipList) {
|
||||
this.config.tcp.allowIPList = ipList
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="ddosProtectionJSON" :value="JSON.stringify(config)"/>
|
||||
|
||||
<p class="comment">功能说明:此功能为<strong>试验性质</strong>,目前仅能防御简单的DDoS攻击,试验期间建议仅在被攻击时启用,仅支持已安装<code-label>nftables v0.9</code-label>以上的Linux系统。<pro-warning-label></pro-warning-label></p>
|
||||
|
||||
<div class="ui message" v-if="vClusterIsOn">当前节点所在集群已设置DDoS防护。</div>
|
||||
|
||||
<h4>TCP设置</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config.tcp" v-if="isNode"></prior-checkbox>
|
||||
<tbody v-show="config.tcp.isPrior || !isNode">
|
||||
<tr>
|
||||
<td class="title">启用DDoS防护</td>
|
||||
<td>
|
||||
<checkbox v-model="config.tcp.isOn"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.tcp.isOn && (config.tcp.isPrior || !isNode)">
|
||||
<tr>
|
||||
<td class="title">单节点TCP最大连接数</td>
|
||||
<td>
|
||||
<digit-input name="tcpMaxConnections" v-model="config.tcp.maxConnections" maxlength="6" size="6" style="width: 6em"></digit-input>
|
||||
<p class="comment">单个节点可以接受的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnections}}。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP TCP最大连接数</td>
|
||||
<td>
|
||||
<digit-input name="tcpMaxConnectionsPerIP" v-model="config.tcp.maxConnectionsPerIP" maxlength="6" size="6" style="width: 6em"></digit-input>
|
||||
<p class="comment">单个IP可以连接到节点的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnectionsPerIP}};最小值为{{defaultConfigs.tcpMinConnectionsPerIP}}。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP TCP新连接速率<em>(分钟)</em></td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<div class="ui input right labeled">
|
||||
<digit-input name="tcpNewConnectionsRate" v-model="config.tcp.newConnectionsRate" maxlength="6" size="6" style="width: 6em" :min="defaultConfigs.tcpNewConnectionsMinRate"></digit-input>
|
||||
<span class="ui label">个新连接/每分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field" style="line-height: 2.4em">
|
||||
屏蔽
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input right labeled">
|
||||
<digit-input name="tcpNewConnectionsRateBlockTimeout" v-model="config.tcp.newConnectionsRateBlockTimeout" maxlength="6" size="6" style="width: 5em"></digit-input>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="comment">单个IP每分钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsMinutelyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinMinutelyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP TCP新连接速率<em>(秒钟)</em></td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<div class="ui input right labeled">
|
||||
<digit-input name="tcpNewConnectionsSecondlyRate" v-model="config.tcp.newConnectionsSecondlyRate" maxlength="6" size="6" style="width: 6em" :min="defaultConfigs.tcpNewConnectionsMinRate"></digit-input>
|
||||
<span class="ui label">个新连接/每秒钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field" style="line-height: 2.4em">
|
||||
屏蔽
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input right labeled">
|
||||
<digit-input name="tcpNewConnectionsSecondlyRateBlockTimeout" v-model="config.tcp.newConnectionsSecondlyRateBlockTimeout" maxlength="6" size="6" style="width: 5em"></digit-input>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="comment">单个IP每秒钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsSecondlyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinSecondlyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TCP端口列表</td>
|
||||
<td>
|
||||
<ddos-protection-ports-config-box :v-ports="config.tcp.ports" @change="changeTCPPorts"></ddos-protection-ports-config-box>
|
||||
<p class="comment">在这些端口上使用当前配置。默认为80和443两个端口。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP白名单</td>
|
||||
<td>
|
||||
<ddos-protection-ip-list-config-box :v-ip-list="config.tcp.allowIPList" @change="changeTCPAllowIPList"></ddos-protection-ip-list-config-box>
|
||||
<p class="comment">在白名单中的IP不受当前设置的限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
Vue.component("bandwidth-size-capacity-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (v.unit == null || v.unit.length == 0) {
|
||||
v.unit = "mb"
|
||||
}
|
||||
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let vSize = this.size
|
||||
if (vSize == null) {
|
||||
vSize = 6
|
||||
}
|
||||
|
||||
let vMaxlength = this.maxlength
|
||||
if (vMaxlength == null) {
|
||||
vMaxlength = 10
|
||||
}
|
||||
|
||||
let supportedUnits = this.vSupportedUnits
|
||||
if (supportedUnits == null) {
|
||||
supportedUnits = []
|
||||
}
|
||||
|
||||
return {
|
||||
capacity: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
vSize: vSize,
|
||||
vMaxlength: vMaxlength,
|
||||
supportedUnits: supportedUnits
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.capacity.count = -1
|
||||
this.change()
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.capacity.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.capacity)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="capacity.unit" @change="change">
|
||||
<option value="b" v-if="supportedUnits.length == 0 || supportedUnits.$contains('b')">Bps</option>
|
||||
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">Kbps</option>
|
||||
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">Mbps</option>
|
||||
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">Gbps</option>
|
||||
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">Tbps</option>
|
||||
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">Pbps</option>
|
||||
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">Ebps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
Vue.component("bandwidth-size-capacity-view", {
|
||||
props: ["v-value"],
|
||||
data: function () {
|
||||
let capacity = this.vValue
|
||||
if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") {
|
||||
capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps"
|
||||
}
|
||||
return {
|
||||
capacity: capacity
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
<span v-if="capacity != null && capacity.count > 0">{{capacity.count}}{{capacity.unit}}</span>
|
||||
</span>`
|
||||
})
|
||||
16
EdgeAdmin/web/public/js/components/common/bits-var.js
Normal file
16
EdgeAdmin/web/public/js/components/common/bits-var.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Vue.component("bits-var", {
|
||||
props: ["v-bits"],
|
||||
data: function () {
|
||||
let bits = this.vBits
|
||||
if (typeof bits != "number") {
|
||||
bits = 0
|
||||
}
|
||||
let format = teaweb.splitFormat(teaweb.formatBits(bits))
|
||||
return {
|
||||
format: format
|
||||
}
|
||||
},
|
||||
template:`<var class="normal">
|
||||
<span>{{format[0]}}</span>{{format[1]}}
|
||||
</var>`
|
||||
})
|
||||
16
EdgeAdmin/web/public/js/components/common/bytes-var.js
Normal file
16
EdgeAdmin/web/public/js/components/common/bytes-var.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Vue.component("bytes-var", {
|
||||
props: ["v-bytes"],
|
||||
data: function () {
|
||||
let bytes = this.vBytes
|
||||
if (typeof bytes != "number") {
|
||||
bytes = 0
|
||||
}
|
||||
let format = teaweb.splitFormat(teaweb.formatBytes(bytes))
|
||||
return {
|
||||
format: format
|
||||
}
|
||||
},
|
||||
template:`<var class="normal">
|
||||
<span>{{format[0]}}</span>{{format[1]}}
|
||||
</var>`
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
Vue.component("chart-columns-grid", {
|
||||
props: [],
|
||||
mounted: function () {
|
||||
this.columns = this.calculateColumns()
|
||||
|
||||
let that = this
|
||||
window.addEventListener("resize", function () {
|
||||
that.columns = that.calculateColumns()
|
||||
})
|
||||
},
|
||||
updated: function () {
|
||||
let totalElements = this.$el.getElementsByClassName("column").length
|
||||
if (totalElements == this.totalElements) {
|
||||
return
|
||||
}
|
||||
this.totalElements = totalElements
|
||||
this.calculateColumns()
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
columns: "four",
|
||||
totalElements: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateColumns: function () {
|
||||
let w = window.innerWidth
|
||||
let columns = Math.floor(w / 500)
|
||||
if (columns == 0) {
|
||||
columns = 1
|
||||
}
|
||||
|
||||
let columnElements = this.$el.getElementsByClassName("column")
|
||||
if (columnElements.length == 0) {
|
||||
return "one"
|
||||
}
|
||||
let maxColumns = columnElements.length
|
||||
if (columns > maxColumns) {
|
||||
columns = maxColumns
|
||||
}
|
||||
|
||||
// 添加右侧边框
|
||||
for (let index = 0; index < columnElements.length; index++) {
|
||||
let el = columnElements[index]
|
||||
el.className = el.className.replace("with-border", "")
|
||||
if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
|
||||
el.className += " with-border"
|
||||
}
|
||||
}
|
||||
|
||||
switch (columns) {
|
||||
case 1:
|
||||
return "one"
|
||||
case 2:
|
||||
return "two"
|
||||
case 3:
|
||||
return "three"
|
||||
case 4:
|
||||
return "four"
|
||||
case 5:
|
||||
return "five"
|
||||
case 6:
|
||||
return "six"
|
||||
case 7:
|
||||
return "seven"
|
||||
case 8:
|
||||
return "eight"
|
||||
case 9:
|
||||
return "nine"
|
||||
case 10:
|
||||
return "ten"
|
||||
default:
|
||||
return "ten"
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div :class="'ui ' + columns + ' columns grid chart-grid'">
|
||||
<slot></slot>
|
||||
</div>`
|
||||
})
|
||||
52
EdgeAdmin/web/public/js/components/common/checkbox.js
Normal file
52
EdgeAdmin/web/public/js/components/common/checkbox.js
Normal file
@@ -0,0 +1,52 @@
|
||||
let checkboxId = 0
|
||||
Vue.component("checkbox", {
|
||||
props: ["name", "value", "v-value", "id", "checked"],
|
||||
data: function () {
|
||||
checkboxId++
|
||||
let elementId = this.id
|
||||
if (elementId == null) {
|
||||
elementId = "checkbox" + checkboxId
|
||||
}
|
||||
|
||||
let elementValue = this.vValue
|
||||
if (elementValue == null) {
|
||||
elementValue = "1"
|
||||
}
|
||||
|
||||
let checkedValue = this.value
|
||||
if (checkedValue == null && this.checked == "checked") {
|
||||
checkedValue = elementValue
|
||||
}
|
||||
|
||||
return {
|
||||
elementId: elementId,
|
||||
elementValue: elementValue,
|
||||
newValue: checkedValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.newValue)
|
||||
},
|
||||
check: function () {
|
||||
this.newValue = this.elementValue
|
||||
},
|
||||
uncheck: function () {
|
||||
this.newValue = ""
|
||||
},
|
||||
isChecked: function () {
|
||||
return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (v) {
|
||||
if (typeof v == "boolean") {
|
||||
this.newValue = v
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div class="ui checkbox">
|
||||
<input type="checkbox" :name="name" :value="elementValue" :id="elementId" @change="change" v-model="newValue"/>
|
||||
<label :for="elementId"><slot></slot></label>
|
||||
</div>`
|
||||
})
|
||||
71
EdgeAdmin/web/public/js/components/common/columns-grid.js
Normal file
71
EdgeAdmin/web/public/js/components/common/columns-grid.js
Normal file
@@ -0,0 +1,71 @@
|
||||
Vue.component("columns-grid", {
|
||||
props: [],
|
||||
mounted: function () {
|
||||
this.columns = this.calculateColumns()
|
||||
|
||||
let that = this
|
||||
window.addEventListener("resize", function () {
|
||||
that.columns = that.calculateColumns()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
columns: "four"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateColumns: function () {
|
||||
let w = window.innerWidth
|
||||
let columns = Math.floor(w / 250)
|
||||
if (columns == 0) {
|
||||
columns = 1
|
||||
}
|
||||
|
||||
let columnElements = this.$el.getElementsByClassName("column")
|
||||
if (columnElements.length == 0) {
|
||||
return
|
||||
}
|
||||
let maxColumns = columnElements.length
|
||||
if (columns > maxColumns) {
|
||||
columns = maxColumns
|
||||
}
|
||||
|
||||
// 添加右侧边框
|
||||
for (let index = 0; index < columnElements.length; index++) {
|
||||
let el = columnElements[index]
|
||||
el.className = el.className.replace("with-border", "")
|
||||
if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
|
||||
el.className += " with-border"
|
||||
}
|
||||
}
|
||||
|
||||
switch (columns) {
|
||||
case 1:
|
||||
return "one"
|
||||
case 2:
|
||||
return "two"
|
||||
case 3:
|
||||
return "three"
|
||||
case 4:
|
||||
return "four"
|
||||
case 5:
|
||||
return "five"
|
||||
case 6:
|
||||
return "six"
|
||||
case 7:
|
||||
return "seven"
|
||||
case 8:
|
||||
return "eight"
|
||||
case 9:
|
||||
return "nine"
|
||||
case 10:
|
||||
return "ten"
|
||||
default:
|
||||
return "ten"
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div :class="'ui ' + columns + ' columns grid counter-chart'">
|
||||
<slot></slot>
|
||||
</div>`
|
||||
})
|
||||
273
EdgeAdmin/web/public/js/components/common/combo-box.js
Normal file
273
EdgeAdmin/web/public/js/components/common/combo-box.js
Normal file
@@ -0,0 +1,273 @@
|
||||
Vue.component("combo-box", {
|
||||
// data-url 和 data-key 成对出现
|
||||
props: [
|
||||
"name", "title", "placeholder", "size", "v-items", "v-value",
|
||||
"data-url", // 数据源URL
|
||||
"data-key", // 数据源中数据的键名
|
||||
"data-search", // 是否启用动态搜索,如果值为on或true,则表示启用
|
||||
"width"
|
||||
],
|
||||
mounted: function () {
|
||||
if (this.dataURL.length > 0) {
|
||||
this.search("")
|
||||
}
|
||||
|
||||
// 设定菜单宽度
|
||||
let searchBox = this.$refs.searchBox
|
||||
if (searchBox != null) {
|
||||
let inputWidth = searchBox.offsetWidth
|
||||
if (inputWidth != null && inputWidth > 0) {
|
||||
this.$refs.menu.style.width = inputWidth + "px"
|
||||
} else if (this.styleWidth.length > 0) {
|
||||
this.$refs.menu.style.width = this.styleWidth
|
||||
}
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
let items = this.vItems
|
||||
if (items == null || !(items instanceof Array)) {
|
||||
items = []
|
||||
}
|
||||
items = this.formatItems(items)
|
||||
|
||||
// 当前选中项
|
||||
let selectedItem = null
|
||||
if (this.vValue != null) {
|
||||
let that = this
|
||||
items.forEach(function (v) {
|
||||
if (v.value == that.vValue) {
|
||||
selectedItem = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let width = this.width
|
||||
if (width == null || width.length == 0) {
|
||||
width = "11em"
|
||||
} else {
|
||||
if (/\d+$/.test(width)) {
|
||||
width += "em"
|
||||
}
|
||||
}
|
||||
|
||||
// data url
|
||||
let dataURL = ""
|
||||
if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) {
|
||||
dataURL = this.dataUrl
|
||||
}
|
||||
|
||||
return {
|
||||
allItems: items, // 原始的所有的items
|
||||
items: items.$copy(), // 候选的items
|
||||
selectedItem: selectedItem, // 选中的item
|
||||
keyword: "",
|
||||
visible: false,
|
||||
hideTimer: null,
|
||||
hoverIndex: 0,
|
||||
styleWidth: width,
|
||||
|
||||
isInitial: true,
|
||||
dataURL: dataURL,
|
||||
urlRequestId: 0 // 记录URL请求ID,防止并行冲突
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search: function (keyword) {
|
||||
// 从URL中获取选项数据
|
||||
let dataUrl = this.dataURL
|
||||
let dataKey = this.dataKey
|
||||
let that = this
|
||||
|
||||
let requestId = Math.random()
|
||||
this.urlRequestId = requestId
|
||||
|
||||
Tea.action(dataUrl)
|
||||
.params({
|
||||
keyword: (keyword == null) ? "" : keyword
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
if (requestId != that.urlRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (resp.data != null) {
|
||||
if (typeof (resp.data[dataKey]) == "object") {
|
||||
let items = that.formatItems(resp.data[dataKey])
|
||||
that.allItems = items
|
||||
that.items = items.$copy()
|
||||
|
||||
if (that.isInitial) {
|
||||
that.isInitial = false
|
||||
if (that.vValue != null) {
|
||||
items.forEach(function (v) {
|
||||
if (v.value == that.vValue) {
|
||||
that.selectedItem = v
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
formatItems: function (items) {
|
||||
items.forEach(function (v) {
|
||||
if (v.value == null) {
|
||||
v.value = v.id
|
||||
}
|
||||
})
|
||||
return items
|
||||
},
|
||||
reset: function () {
|
||||
this.selectedItem = null
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
if (that.$refs.searchBox) {
|
||||
that.$refs.searchBox.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
clear: function () {
|
||||
this.selectedItem = null
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
},
|
||||
changeKeyword: function () {
|
||||
let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true")
|
||||
|
||||
this.hoverIndex = 0
|
||||
let keyword = this.keyword
|
||||
if (keyword.length == 0) {
|
||||
if (shouldSearch) {
|
||||
this.search(keyword)
|
||||
} else {
|
||||
this.items = this.allItems.$copy()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (shouldSearch) {
|
||||
this.search(keyword)
|
||||
} else {
|
||||
this.items = this.allItems.$copy().filter(function (v) {
|
||||
if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) {
|
||||
return true
|
||||
}
|
||||
return teaweb.match(v.name, keyword)
|
||||
})
|
||||
}
|
||||
},
|
||||
selectItem: function (item) {
|
||||
this.selectedItem = item
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
this.keyword = ""
|
||||
this.changeKeyword()
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.items.length > this.hoverIndex) {
|
||||
this.selectItem(this.items[this.hoverIndex])
|
||||
}
|
||||
},
|
||||
show: function () {
|
||||
this.visible = true
|
||||
|
||||
// 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换
|
||||
},
|
||||
hide: function () {
|
||||
let that = this
|
||||
this.hideTimer = setTimeout(function () {
|
||||
that.visible = false
|
||||
}, 500)
|
||||
},
|
||||
downItem: function () {
|
||||
this.hoverIndex++
|
||||
if (this.hoverIndex > this.items.length - 1) {
|
||||
this.hoverIndex = 0
|
||||
}
|
||||
this.focusItem()
|
||||
},
|
||||
upItem: function () {
|
||||
this.hoverIndex--
|
||||
if (this.hoverIndex < 0) {
|
||||
this.hoverIndex = 0
|
||||
}
|
||||
this.focusItem()
|
||||
},
|
||||
focusItem: function () {
|
||||
if (this.hoverIndex < this.items.length) {
|
||||
this.$refs.itemRef[this.hoverIndex].focus()
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.searchBox.focus()
|
||||
if (that.hideTimer != null) {
|
||||
clearTimeout(that.hideTimer)
|
||||
that.hideTimer = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
change: function () {
|
||||
this.$emit("change", this.selectedItem)
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
if (that.$refs.selectedLabel != null) {
|
||||
that.$refs.selectedLabel.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
submitForm: function (event) {
|
||||
if (event.target.tagName != "A") {
|
||||
return
|
||||
}
|
||||
let parentBox = this.$refs.selectedLabel.parentNode
|
||||
while (true) {
|
||||
parentBox = parentBox.parentNode
|
||||
if (parentBox == null || parentBox.tagName == "BODY") {
|
||||
return
|
||||
}
|
||||
if (parentBox.tagName == "FORM") {
|
||||
parentBox.submit()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setDataURL: function (dataURL) {
|
||||
this.dataURL = dataURL
|
||||
},
|
||||
reloadData: function () {
|
||||
this.search("")
|
||||
}
|
||||
},
|
||||
template: `<div style="display: inline; z-index: 10; background: white" class="combo-box">
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="selectedItem == null">
|
||||
<input type="text" v-model="keyword" :placeholder="placeholder" :size="size" :style="{'width': styleWidth}" @input="changeKeyword" @focus="show" @blur="hide" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="searchBox" @keydown.down.prevent="downItem" @keydown.up.prevent="upItem"/>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中 -->
|
||||
<div v-if="selectedItem != null">
|
||||
<input type="hidden" :name="name" :value="selectedItem.value"/>
|
||||
<span class="ui label basic" style="line-height: 1.4; font-weight: normal; font-size: 1em" ref="selectedLabel"><span><span v-if="title != null && title.length > 0">{{title}}:</span>{{selectedItem.name}}</span>
|
||||
<a href="" title="清除" @click.prevent="reset"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div v-show="selectedItem == null && items.length > 0 && visible">
|
||||
<div class="ui menu vertical small narrow-scrollbar" ref="menu">
|
||||
<a href="" v-for="(item, index) in items" ref="itemRef" class="item" :class="{active: index == hoverIndex, blue: index == hoverIndex}" @click.prevent="selectItem(item)" style="line-height: 1.4">
|
||||
<span v-if="item.fullname != null && item.fullname.length > 0">{{item.fullname}}</span>
|
||||
<span v-else>{{item.name}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
Vue.component("copy-to-clipboard", {
|
||||
props: ["v-target"],
|
||||
created: function () {
|
||||
if (typeof ClipboardJS == "undefined") {
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/clipboard.min.js")
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: function () {
|
||||
new ClipboardJS('[data-clipboard-target]');
|
||||
teaweb.successToast("已复制到剪切板")
|
||||
}
|
||||
},
|
||||
template: `<a href="" title="拷贝到剪切板" :data-clipboard-target="'#' + vTarget" @click.prevent="copy"><i class="ui icon copy small"></i></em></a>`
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
Vue.component("countries-selector", {
|
||||
props: ["v-countries"],
|
||||
data: function () {
|
||||
let countries = this.vCountries
|
||||
if (countries == null) {
|
||||
countries = []
|
||||
}
|
||||
let countryIds = countries.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
return {
|
||||
countries: countries,
|
||||
countryIds: countryIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
let countryStringIds = this.countryIds.map(function (v) {
|
||||
return v.toString()
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), {
|
||||
width: "48em",
|
||||
height: "23em",
|
||||
callback: function (resp) {
|
||||
that.countries = resp.data.countries
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.countries.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.countryIds = this.countries.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="countryIdsJSON" :value="JSON.stringify(countryIds)"/>
|
||||
<div v-if="countries.length > 0" style="margin-bottom: 0.5em">
|
||||
<div v-for="(country, index) in countries" class="ui label tiny basic">{{country.name}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
32
EdgeAdmin/web/public/js/components/common/csrf-token.js
Normal file
32
EdgeAdmin/web/public/js/components/common/csrf-token.js
Normal file
@@ -0,0 +1,32 @@
|
||||
Vue.component("csrf-token", {
|
||||
created: function () {
|
||||
this.refreshToken()
|
||||
},
|
||||
mounted: function () {
|
||||
let that = this
|
||||
this.$refs.token.form.addEventListener("submit", function () {
|
||||
that.refreshToken()
|
||||
})
|
||||
|
||||
// 自动刷新
|
||||
setInterval(function () {
|
||||
that.refreshToken()
|
||||
}, 10 * 60 * 1000)
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
token: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshToken: function () {
|
||||
let that = this
|
||||
Tea.action("/csrf/token")
|
||||
.get()
|
||||
.success(function (resp) {
|
||||
that.token = resp.data.token
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<input type="hidden" name="csrfToken" :value="token" ref="token"/>`
|
||||
})
|
||||
59
EdgeAdmin/web/public/js/components/common/datepicker.js
Normal file
59
EdgeAdmin/web/public/js/components/common/datepicker.js
Normal file
@@ -0,0 +1,59 @@
|
||||
Vue.component("datepicker", {
|
||||
props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
teaweb.datepicker(this.$refs.dayInput, function (v) {
|
||||
that.day = v
|
||||
that.change()
|
||||
}, !!this.vBottomLeft)
|
||||
},
|
||||
data: function () {
|
||||
let name = this.vName
|
||||
if (name == null) {
|
||||
name = this.name
|
||||
}
|
||||
if (name == null) {
|
||||
name = "day"
|
||||
}
|
||||
|
||||
let day = this.vValue
|
||||
if (day == null) {
|
||||
day = this.value
|
||||
if (day == null) {
|
||||
day = ""
|
||||
}
|
||||
}
|
||||
|
||||
let placeholder = "YYYY-MM-DD"
|
||||
if (this.placeholder != null) {
|
||||
placeholder = this.placeholder
|
||||
}
|
||||
|
||||
return {
|
||||
realName: name,
|
||||
realPlaceholder: placeholder,
|
||||
day: day
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (v) {
|
||||
this.day = v
|
||||
|
||||
let picker = this.$refs.dayInput.picker
|
||||
if (picker != null) {
|
||||
if (v != null && /^\d+-\d+-\d+$/.test(v)) {
|
||||
picker.setDate(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前
|
||||
this.$emit("change", this.day)
|
||||
}
|
||||
},
|
||||
template: `<div style="display: inline-block">
|
||||
<input type="text" :name="realName" v-model="day" :placeholder="realPlaceholder" style="width:8.6em" maxlength="10" @input="change" ref="dayInput" autocomplete="off"/>
|
||||
</div>`
|
||||
})
|
||||
185
EdgeAdmin/web/public/js/components/common/datetime-input.js
Normal file
185
EdgeAdmin/web/public/js/components/common/datetime-input.js
Normal file
@@ -0,0 +1,185 @@
|
||||
Vue.component("datetime-input", {
|
||||
props: ["v-name", "v-timestamp"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
teaweb.datepicker(this.$refs.dayInput, function (v) {
|
||||
that.day = v
|
||||
that.hour = "23"
|
||||
that.minute = "59"
|
||||
that.second = "59"
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let timestamp = this.vTimestamp
|
||||
if (timestamp != null) {
|
||||
timestamp = parseInt(timestamp)
|
||||
if (isNaN(timestamp)) {
|
||||
timestamp = 0
|
||||
}
|
||||
} else {
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
let day = ""
|
||||
let hour = ""
|
||||
let minute = ""
|
||||
let second = ""
|
||||
|
||||
if (timestamp > 0) {
|
||||
let date = new Date()
|
||||
date.setTime(timestamp * 1000)
|
||||
|
||||
let year = date.getFullYear().toString()
|
||||
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
|
||||
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
|
||||
|
||||
hour = this.leadingZero(date.getHours().toString(), 2)
|
||||
minute = this.leadingZero(date.getMinutes().toString(), 2)
|
||||
second = this.leadingZero(date.getSeconds().toString(), 2)
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: timestamp,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
|
||||
hasDayError: false,
|
||||
hasHourError: false,
|
||||
hasMinuteError: false,
|
||||
hasSecondError: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
// day
|
||||
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
let pieces = this.day.split("-")
|
||||
let year = parseInt(pieces[0])
|
||||
|
||||
let month = parseInt(pieces[1])
|
||||
if (month < 1 || month > 12) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
|
||||
let day = parseInt(pieces[2])
|
||||
if (day < 1 || day > 32) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
|
||||
this.hasDayError = false
|
||||
|
||||
// hour
|
||||
if (!/^\d+$/.test(this.hour)) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
let hour = parseInt(this.hour)
|
||||
if (isNaN(hour)) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
if (hour < 0 || hour >= 24) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
this.hasHourError = false
|
||||
|
||||
// minute
|
||||
if (!/^\d+$/.test(this.minute)) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
let minute = parseInt(this.minute)
|
||||
if (isNaN(minute)) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
if (minute < 0 || minute >= 60) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
this.hasMinuteError = false
|
||||
|
||||
// second
|
||||
if (!/^\d+$/.test(this.second)) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
let second = parseInt(this.second)
|
||||
if (isNaN(second)) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
if (second < 0 || second >= 60) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
this.hasSecondError = false
|
||||
|
||||
let date = new Date(year, month - 1, day, hour, minute, second)
|
||||
this.timestamp = Math.floor(date.getTime() / 1000)
|
||||
},
|
||||
leadingZero: function (s, l) {
|
||||
s = s.toString()
|
||||
if (l <= s.length) {
|
||||
return s
|
||||
}
|
||||
for (let i = 0; i < l - s.length; i++) {
|
||||
s = "0" + s
|
||||
}
|
||||
return s
|
||||
},
|
||||
resultTimestamp: function () {
|
||||
return this.timestamp
|
||||
},
|
||||
nextYear: function () {
|
||||
let date = new Date()
|
||||
date.setFullYear(date.getFullYear()+1)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
},
|
||||
nextDays: function (days) {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() + days * 86400 * 1000)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
},
|
||||
nextHours: function (hours) {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() + hours * 3600 * 1000)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="vName" :value="timestamp"/>
|
||||
<div class="ui fields inline" style="padding: 0; margin:0">
|
||||
<div class="ui field" :class="{error: hasDayError}">
|
||||
<input type="text" v-model="day" placeholder="YYYY-MM-DD" style="width:8.6em" maxlength="10" @input="change" ref="dayInput"/>
|
||||
</div>
|
||||
<div class="ui field" :class="{error: hasHourError}"><input type="text" v-model="hour" maxlength="2" style="width:4em" placeholder="时" @input="change"/></div>
|
||||
<div class="ui field">:</div>
|
||||
<div class="ui field" :class="{error: hasMinuteError}"><input type="text" v-model="minute" maxlength="2" style="width:4em" placeholder="分" @input="change"/></div>
|
||||
<div class="ui field">:</div>
|
||||
<div class="ui field" :class="{error: hasSecondError}"><input type="text" v-model="second" maxlength="2" style="width:4em" placeholder="秒" @input="change"/></div>
|
||||
</div>
|
||||
<p class="comment">常用时间:<a href="" @click.prevent="nextHours(1)"> 1小时 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(1)"> 1天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(3)"> 3天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(7)"> 1周 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(30)"> 30天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextYear()"> 1年 </a> </p>
|
||||
</div>`
|
||||
})
|
||||
3
EdgeAdmin/web/public/js/components/common/dot.js
Normal file
3
EdgeAdmin/web/public/js/components/common/dot.js
Normal file
@@ -0,0 +1,3 @@
|
||||
Vue.component("dot", {
|
||||
template: '<span style="display: inline-block; padding-bottom: 3px"><i class="icon circle tiny"></i></span>'
|
||||
})
|
||||
42
EdgeAdmin/web/public/js/components/common/download-link.js
Normal file
42
EdgeAdmin/web/public/js/components/common/download-link.js
Normal file
@@ -0,0 +1,42 @@
|
||||
Vue.component("download-link", {
|
||||
props: ["v-element", "v-file", "v-value"],
|
||||
created: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.url = that.composeURL()
|
||||
}, 1000)
|
||||
},
|
||||
data: function () {
|
||||
let filename = this.vFile
|
||||
if (filename == null || filename.length == 0) {
|
||||
filename = "unknown-file"
|
||||
}
|
||||
return {
|
||||
file: filename,
|
||||
url: this.composeURL()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
composeURL: function () {
|
||||
let text = ""
|
||||
if (this.vValue != null) {
|
||||
text = this.vValue
|
||||
} else {
|
||||
let e = document.getElementById(this.vElement)
|
||||
if (e == null) {
|
||||
// 不提示错误,因为此时可能页面未加载完整
|
||||
return
|
||||
}
|
||||
text = e.innerText
|
||||
if (text == null) {
|
||||
text = e.textContent
|
||||
}
|
||||
}
|
||||
return Tea.url("/ui/download", {
|
||||
file: this.file,
|
||||
text: text
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<a :href="url" target="_blank" style="font-weight: normal"><slot></slot></a>`,
|
||||
})
|
||||
30
EdgeAdmin/web/public/js/components/common/file-textarea.js
Normal file
30
EdgeAdmin/web/public/js/components/common/file-textarea.js
Normal file
@@ -0,0 +1,30 @@
|
||||
Vue.component("file-textarea", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let value = this.value
|
||||
if (typeof value != "string") {
|
||||
value = ""
|
||||
}
|
||||
return {
|
||||
realValue: value
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
},
|
||||
methods: {
|
||||
dragover: function () {},
|
||||
drop: function (e) {
|
||||
let that = this
|
||||
e.dataTransfer.items[0].getAsFile().text().then(function (data) {
|
||||
that.setValue(data)
|
||||
})
|
||||
},
|
||||
setValue: function (value) {
|
||||
this.realValue = value
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.textarea.focus()
|
||||
}
|
||||
},
|
||||
template: `<textarea @drop.prevent="drop" @dragover.prevent="dragover" ref="textarea" v-model="realValue"></textarea>`
|
||||
})
|
||||
13
EdgeAdmin/web/public/js/components/common/first-menu.js
Normal file
13
EdgeAdmin/web/public/js/components/common/first-menu.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 一级菜单
|
||||
*/
|
||||
Vue.component("first-menu", {
|
||||
props: [],
|
||||
template: ' \
|
||||
<div class="first-menu"> \
|
||||
<div class="ui menu text blue small">\
|
||||
<slot></slot>\
|
||||
</div> \
|
||||
<div class="ui divider"></div> \
|
||||
</div>'
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
Vue.component("health-check-config-box", {
|
||||
props: ["v-health-check-config", "v-check-domain-url", "v-is-plus"],
|
||||
data: function () {
|
||||
let healthCheckConfig = this.vHealthCheckConfig
|
||||
let urlProtocol = "http"
|
||||
let urlPort = ""
|
||||
let urlRequestURI = "/"
|
||||
let urlHost = ""
|
||||
|
||||
if (healthCheckConfig == null) {
|
||||
healthCheckConfig = {
|
||||
isOn: false,
|
||||
url: "",
|
||||
interval: {count: 60, unit: "second"},
|
||||
statusCodes: [200],
|
||||
timeout: {count: 10, unit: "second"},
|
||||
countTries: 3,
|
||||
tryDelay: {count: 100, unit: "ms"},
|
||||
autoDown: true,
|
||||
countUp: 1,
|
||||
countDown: 3,
|
||||
userAgent: "",
|
||||
onlyBasicRequest: true,
|
||||
accessLogIsOn: true
|
||||
}
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.changeURL()
|
||||
}, 500)
|
||||
} else {
|
||||
try {
|
||||
let url = new URL(healthCheckConfig.url)
|
||||
urlProtocol = url.protocol.substring(0, url.protocol.length - 1)
|
||||
|
||||
// 域名
|
||||
urlHost = url.host
|
||||
if (urlHost == "%24%7Bhost%7D") {
|
||||
urlHost = "${host}"
|
||||
}
|
||||
let colonIndex = urlHost.indexOf(":")
|
||||
if (colonIndex > 0) {
|
||||
urlHost = urlHost.substring(0, colonIndex)
|
||||
}
|
||||
|
||||
urlPort = url.port
|
||||
urlRequestURI = url.pathname
|
||||
if (url.search.length > 0) {
|
||||
urlRequestURI += url.search
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (healthCheckConfig.statusCodes == null) {
|
||||
healthCheckConfig.statusCodes = [200]
|
||||
}
|
||||
if (healthCheckConfig.interval == null) {
|
||||
healthCheckConfig.interval = {count: 60, unit: "second"}
|
||||
}
|
||||
if (healthCheckConfig.timeout == null) {
|
||||
healthCheckConfig.timeout = {count: 10, unit: "second"}
|
||||
}
|
||||
if (healthCheckConfig.tryDelay == null) {
|
||||
healthCheckConfig.tryDelay = {count: 100, unit: "ms"}
|
||||
}
|
||||
if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) {
|
||||
healthCheckConfig.countUp = 1
|
||||
}
|
||||
if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) {
|
||||
healthCheckConfig.countDown = 3
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
healthCheck: healthCheckConfig,
|
||||
advancedVisible: false,
|
||||
urlProtocol: urlProtocol,
|
||||
urlHost: urlHost,
|
||||
urlPort: urlPort,
|
||||
urlRequestURI: urlRequestURI,
|
||||
urlIsEditing: healthCheckConfig.url.length == 0,
|
||||
|
||||
hostErr: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
urlRequestURI: function () {
|
||||
if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") {
|
||||
this.urlRequestURI = "/" + this.urlRequestURI
|
||||
}
|
||||
this.changeURL()
|
||||
},
|
||||
urlPort: function (v) {
|
||||
let port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
this.urlPort = port.toString()
|
||||
} else {
|
||||
this.urlPort = ""
|
||||
}
|
||||
this.changeURL()
|
||||
},
|
||||
urlProtocol: function () {
|
||||
this.changeURL()
|
||||
},
|
||||
urlHost: function () {
|
||||
this.changeURL()
|
||||
this.hostErr = ""
|
||||
},
|
||||
"healthCheck.countTries": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (!isNaN(count)) {
|
||||
this.healthCheck.countTries = count
|
||||
} else {
|
||||
this.healthCheck.countTries = 0
|
||||
}
|
||||
},
|
||||
"healthCheck.countUp": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (!isNaN(count)) {
|
||||
this.healthCheck.countUp = count
|
||||
} else {
|
||||
this.healthCheck.countUp = 0
|
||||
}
|
||||
},
|
||||
"healthCheck.countDown": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (!isNaN(count)) {
|
||||
this.healthCheck.countDown = count
|
||||
} else {
|
||||
this.healthCheck.countDown = 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showAdvanced: function () {
|
||||
this.advancedVisible = !this.advancedVisible
|
||||
},
|
||||
changeURL: function () {
|
||||
let urlHost = this.urlHost
|
||||
if (urlHost.length == 0) {
|
||||
urlHost = "${host}"
|
||||
}
|
||||
this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI
|
||||
},
|
||||
changeStatus: function (values) {
|
||||
this.healthCheck.statusCodes = values.$map(function (k, v) {
|
||||
let status = parseInt(v)
|
||||
if (isNaN(status)) {
|
||||
return 0
|
||||
} else {
|
||||
return status
|
||||
}
|
||||
})
|
||||
},
|
||||
onChangeURLHost: function () {
|
||||
let checkDomainURL = this.vCheckDomainUrl
|
||||
if (checkDomainURL == null || checkDomainURL.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
Tea.action(checkDomainURL)
|
||||
.params({host: this.urlHost})
|
||||
.success(function (resp) {
|
||||
if (!resp.data.isOk) {
|
||||
that.hostErr = "在当前集群中找不到此域名,可能会影响健康检查结果。"
|
||||
} else {
|
||||
that.hostErr = ""
|
||||
}
|
||||
})
|
||||
.post()
|
||||
},
|
||||
editURL: function () {
|
||||
this.urlIsEditing = !this.urlIsEditing
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="healthCheckJSON" :value="JSON.stringify(healthCheck)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用健康检查</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="healthCheck.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">通过访问节点上的网站URL来确定节点是否健康。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="healthCheck.isOn">
|
||||
<tr>
|
||||
<td>检测URL *</td>
|
||||
<td>
|
||||
<div v-if="healthCheck.url.length > 0" style="margin-bottom: 1em"><code-label>{{healthCheck.url}}</code-label> <a href="" @click.prevent="editURL"><span class="small">修改 <i class="icon angle" :class="{down: !urlIsEditing, up: urlIsEditing}"></i></span></a> </div>
|
||||
<div v-show="urlIsEditing">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">协议</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="urlProtocol">
|
||||
<option value="http">http://</option>
|
||||
<option value="https">https://</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>域名</td>
|
||||
<td>
|
||||
<input type="text" v-model="urlHost" @change="onChangeURLHost"/>
|
||||
<p class="comment"><span v-if="hostErr.length > 0" class="red">{{hostErr}}</span>已经部署到当前集群的一个域名;如果为空则使用节点IP作为域名。<span class="red" v-if="urlProtocol == 'https' && urlHost.length == 0">如果协议是https,这里必须填写一个已经设置了SSL证书的域名。</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>端口</td>
|
||||
<td>
|
||||
<input type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/>
|
||||
<p class="comment">域名或者IP的端口,可选项,默认为80/443。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RequestURI</td>
|
||||
<td><input type="text" v-model="urlRequestURI" placeholder="/" style="width:20em"/>
|
||||
<p class="comment">请求的路径,可以带参数,可选项。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
<p class="comment" v-if="healthCheck.url.length > 0">拼接后的检测URL:<code-label>{{healthCheck.url}}</code-label>,其中\${host}指的是域名。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>检测时间间隔</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="healthCheck.interval"></time-duration-box>
|
||||
<p class="comment">两次检查之间的间隔。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动上/下线<span v-if="vIsPlus">IP</span></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="healthCheck.autoDown"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后系统会根据健康检查的结果自动标记<span v-if="vIsPlus">节点IP</span><span v-else>节点</span>的上线/下线状态,并可能自动同步DNS设置。<span v-if="!vIsPlus">注意:免费版的只能整体上下线整个节点,商业版的可以下线单个IP。</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="healthCheck.autoDown">
|
||||
<td>连续上线次数</td>
|
||||
<td>
|
||||
<input type="text" v-model="healthCheck.countUp" style="width:5em" maxlength="6"/>
|
||||
<p class="comment">连续{{healthCheck.countUp}}次检查成功后自动恢复上线。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="healthCheck.autoDown">
|
||||
<td>连续下线次数</td>
|
||||
<td>
|
||||
<input type="text" v-model="healthCheck.countDown" style="width:5em" maxlength="6"/>
|
||||
<p class="comment">连续{{healthCheck.countDown}}次检查失败后自动下线。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="healthCheck.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-angle @change="showAdvanced"></more-options-angle></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="advancedVisible && healthCheck.isOn">
|
||||
<tr>
|
||||
<td>允许的状态码</td>
|
||||
<td>
|
||||
<values-box :values="healthCheck.statusCodes" maxlength="3" @change="changeStatus"></values-box>
|
||||
<p class="comment">允许检测URL返回的状态码列表。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>超时时间</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="healthCheck.timeout"></time-duration-box>
|
||||
<p class="comment">读取检测URL超时时间。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>连续尝试次数</td>
|
||||
<td>
|
||||
<input type="text" v-model="healthCheck.countTries" style="width: 5em" maxlength="2"/>
|
||||
<p class="comment">如果读取检测URL失败后需要再次尝试的次数。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>每次尝试间隔</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="healthCheck.tryDelay"></time-duration-box>
|
||||
<p class="comment">如果读取检测URL失败后再次尝试时的间隔时间。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>终端信息<em>(User-Agent)</em></td>
|
||||
<td>
|
||||
<input type="text" v-model="healthCheck.userAgent" maxlength="200"/>
|
||||
<p class="comment">发送到服务器的User-Agent值,不填写表示使用默认值。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>只基础请求</td>
|
||||
<td>
|
||||
<checkbox v-model="healthCheck.onlyBasicRequest"></checkbox>
|
||||
<p class="comment">只做基础的请求,不处理反向代理(不检查源站)、WAF等。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>记录访问日志</td>
|
||||
<td>
|
||||
<checkbox v-model="healthCheck.accessLogIsOn"></checkbox>
|
||||
<p class="comment">记录健康检查的访问日志。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
23
EdgeAdmin/web/public/js/components/common/inner-menu-item.js
Normal file
23
EdgeAdmin/web/public/js/components/common/inner-menu-item.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 菜单项
|
||||
*/
|
||||
Vue.component("inner-menu-item", {
|
||||
props: ["href", "active", "code"],
|
||||
data: function () {
|
||||
var active = this.active;
|
||||
if (typeof(active) =="undefined") {
|
||||
var itemCode = "";
|
||||
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
|
||||
itemCode = window.TEA.ACTION.data.firstMenuItem;
|
||||
}
|
||||
active = (itemCode == this.code);
|
||||
}
|
||||
return {
|
||||
vHref: (this.href == null) ? "" : this.href,
|
||||
vActive: active
|
||||
};
|
||||
},
|
||||
template: '\
|
||||
<a :href="vHref" class="item right" style="color:#4183c4" :class="{active:vActive}">[<slot></slot>]</a> \
|
||||
'
|
||||
});
|
||||
11
EdgeAdmin/web/public/js/components/common/inner-menu.js
Normal file
11
EdgeAdmin/web/public/js/components/common/inner-menu.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 二级菜单
|
||||
*/
|
||||
Vue.component("inner-menu", {
|
||||
template: `
|
||||
<div class="second-menu" style="width:80%;position: absolute;top:-8px;right:1em">
|
||||
<div class="ui menu text blue small">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>`
|
||||
});
|
||||
63
EdgeAdmin/web/public/js/components/common/inputs.js
Normal file
63
EdgeAdmin/web/public/js/components/common/inputs.js
Normal file
@@ -0,0 +1,63 @@
|
||||
Vue.component("digit-input", {
|
||||
props: ["value", "maxlength", "size", "min", "max", "required", "placeholder"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.check()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let realMaxLength = this.maxlength
|
||||
if (realMaxLength == null) {
|
||||
realMaxLength = 20
|
||||
}
|
||||
|
||||
let realSize = this.size
|
||||
if (realSize == null) {
|
||||
realSize = 6
|
||||
}
|
||||
|
||||
return {
|
||||
realValue: this.value,
|
||||
realMaxLength: realMaxLength,
|
||||
realSize: realSize,
|
||||
isValid: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
realValue: function (v) {
|
||||
this.notifyChange()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
notifyChange: function () {
|
||||
let v = parseInt(this.realValue.toString(), 10)
|
||||
if (isNaN(v)) {
|
||||
v = 0
|
||||
}
|
||||
this.check()
|
||||
this.$emit("input", v)
|
||||
},
|
||||
check: function () {
|
||||
if (this.realValue == null) {
|
||||
return
|
||||
}
|
||||
let s = this.realValue.toString()
|
||||
if (!/^\d+$/.test(s)) {
|
||||
this.isValid = false
|
||||
return
|
||||
}
|
||||
let v = parseInt(s, 10)
|
||||
if (isNaN(v)) {
|
||||
this.isValid = false
|
||||
} else {
|
||||
if (this.required) {
|
||||
this.isValid = (this.min == null || this.min <= v) && (this.max == null || this.max >= v)
|
||||
} else {
|
||||
this.isValid = (v == 0 || (this.min == null || this.min <= v) && (this.max == null || this.max >= v))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<input type="text" v-model="realValue" :maxlength="realMaxLength" :size="realSize" :class="{error: !this.isValid}" :placeholder="placeholder" autocomplete="off"/>`
|
||||
})
|
||||
27
EdgeAdmin/web/public/js/components/common/js-page.js
Normal file
27
EdgeAdmin/web/public/js/components/common/js-page.js
Normal file
@@ -0,0 +1,27 @@
|
||||
Vue.component("js-page", {
|
||||
props: ["v-max"],
|
||||
data: function () {
|
||||
let max = this.vMax
|
||||
if (max == null) {
|
||||
max = 0
|
||||
}
|
||||
return {
|
||||
max: max,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateMax: function (max) {
|
||||
this.max = max
|
||||
},
|
||||
selectPage: function(page) {
|
||||
this.page = page
|
||||
this.$emit("change", page)
|
||||
}
|
||||
},
|
||||
template:`<div>
|
||||
<div class="page" v-if="max > 1">
|
||||
<a href="" v-for="i in max" :class="{active: i == page}" @click.prevent="selectPage(i)">{{i}}</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
58
EdgeAdmin/web/public/js/components/common/keyword.js
Normal file
58
EdgeAdmin/web/public/js/components/common/keyword.js
Normal file
@@ -0,0 +1,58 @@
|
||||
Vue.component("keyword", {
|
||||
props: ["v-word"],
|
||||
data: function () {
|
||||
let word = this.vWord
|
||||
if (word == null) {
|
||||
word = ""
|
||||
} else {
|
||||
word = word.replace(/\)/g, "\\)")
|
||||
word = word.replace(/\(/g, "\\(")
|
||||
word = word.replace(/\+/g, "\\+")
|
||||
word = word.replace(/\^/g, "\\^")
|
||||
word = word.replace(/\$/g, "\\$")
|
||||
word = word.replace(/\?/g, "\\?")
|
||||
word = word.replace(/\*/g, "\\*")
|
||||
word = word.replace(/\[/g, "\\[")
|
||||
word = word.replace(/{/g, "\\{")
|
||||
word = word.replace(/\./g, "\\.")
|
||||
}
|
||||
|
||||
let slot = this.$slots["default"][0]
|
||||
let text = slot.text
|
||||
if (word.length > 0) {
|
||||
let that = this
|
||||
let m = [] // replacement => tmp
|
||||
let tmpIndex = 0
|
||||
text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) {
|
||||
tmpIndex++
|
||||
let s = "<span style=\"border: 1px #ccc dashed; color: #ef4d58\">" + that.encodeHTML(replacement) + "</span>"
|
||||
let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$"
|
||||
m.push([tmpKey, s])
|
||||
return tmpKey
|
||||
})
|
||||
text = this.encodeHTML(text)
|
||||
|
||||
m.forEach(function (r) {
|
||||
text = text.replace(r[0], r[1])
|
||||
})
|
||||
|
||||
} else {
|
||||
text = this.encodeHTML(text)
|
||||
}
|
||||
|
||||
return {
|
||||
word: word,
|
||||
text: text
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
encodeHTML: function (s) {
|
||||
s = s.replace(/&/g, "&")
|
||||
s = s.replace(/</g, "<")
|
||||
s = s.replace(/>/g, ">")
|
||||
s = s.replace(/"/g, """)
|
||||
return s
|
||||
}
|
||||
},
|
||||
template: `<span><span style="display: none"><slot></slot></span><span v-html="text"></span></span>`
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
Vue.component("labeled-input", {
|
||||
props: ["name", "size", "maxlength", "label", "value"],
|
||||
template: '<div class="ui input right labeled"> \
|
||||
<input type="text" :name="name" :size="size" :maxlength="maxlength" :value="value"/>\
|
||||
<span class="ui label">{{label}}</span>\
|
||||
</div>'
|
||||
});
|
||||
65
EdgeAdmin/web/public/js/components/common/labels.js
Normal file
65
EdgeAdmin/web/public/js/components/common/labels.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// 启用状态标签
|
||||
Vue.component("label-on", {
|
||||
props: ["v-is-on"],
|
||||
template: '<div><span v-if="vIsOn" class="green">已启用</span><span v-if="!vIsOn" class="red">已停用</span></div>'
|
||||
})
|
||||
|
||||
// 文字代码标签
|
||||
Vue.component("code-label", {
|
||||
methods: {
|
||||
click: function (args) {
|
||||
this.$emit("click", args)
|
||||
}
|
||||
},
|
||||
template: `<span class="ui label basic small" style="padding: 3px;margin-left:2px;margin-right:2px" @click.prevent="click"><slot></slot></span>`
|
||||
})
|
||||
|
||||
Vue.component("code-label-plain", {
|
||||
template: `<span class="ui label basic tiny" style="padding: 3px;margin-left:2px;margin-right:2px"><slot></slot></span>`
|
||||
})
|
||||
|
||||
|
||||
// tiny标签
|
||||
Vue.component("tiny-label", {
|
||||
template: `<span class="ui label tiny" style="margin-bottom: 0.5em"><slot></slot></span>`
|
||||
})
|
||||
|
||||
Vue.component("tiny-basic-label", {
|
||||
template: `<span class="ui label tiny basic" style="margin-bottom: 0.5em"><slot></slot></span>`
|
||||
})
|
||||
|
||||
// 更小的标签
|
||||
Vue.component("micro-basic-label", {
|
||||
template: `<span class="ui label tiny basic" style="margin-bottom: 0.5em; font-size: 0.7em; padding: 4px"><slot></slot></span>`
|
||||
})
|
||||
|
||||
|
||||
// 灰色的Label
|
||||
Vue.component("grey-label", {
|
||||
props: ["color"],
|
||||
data: function () {
|
||||
let color = "grey"
|
||||
if (this.color != null && this.color.length > 0) {
|
||||
color = "red"
|
||||
}
|
||||
return {
|
||||
labelColor: color
|
||||
}
|
||||
},
|
||||
template: `<span class="ui label basic tiny" :class="labelColor" style="margin-top: 0.4em; font-size: 0.7em; border: 1px solid #ddd!important; font-weight: normal;"><slot></slot></span>`
|
||||
})
|
||||
|
||||
// 可选标签
|
||||
Vue.component("optional-label", {
|
||||
template: `<em><span class="grey">(可选)</span></em>`
|
||||
})
|
||||
|
||||
// Plus专属
|
||||
Vue.component("plus-label", {
|
||||
template: `<span style="color: #B18701;">Plus专属功能。</span>`
|
||||
})
|
||||
|
||||
// 提醒设置项为专业设置
|
||||
Vue.component("pro-warning-label", {
|
||||
template: `<span><i class="icon warning circle yellow"></i>注意:通常不需要修改;如要修改,请在专家指导下进行。</span>`
|
||||
})
|
||||
86
EdgeAdmin/web/public/js/components/common/links.js
Normal file
86
EdgeAdmin/web/public/js/components/common/links.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// 使用Icon的链接方式
|
||||
Vue.component("link-icon", {
|
||||
props: ["href", "title", "target", "size"],
|
||||
data: function () {
|
||||
let realSize = this.size
|
||||
if (realSize == null || realSize.length == 0) {
|
||||
realSize = "small"
|
||||
}
|
||||
|
||||
return {
|
||||
vTitle: (this.title == null) ? "打开链接" : this.title,
|
||||
realSize: realSize
|
||||
}
|
||||
},
|
||||
template: `<span><slot></slot> <a :href="href" :title="vTitle" class="link grey" :target="target"><i class="icon linkify" :class="realSize"></i></a></span>`
|
||||
})
|
||||
|
||||
// 带有下划虚线的连接
|
||||
Vue.component("link-red", {
|
||||
props: ["href", "title"],
|
||||
data: function () {
|
||||
let href = this.href
|
||||
if (href == null) {
|
||||
href = ""
|
||||
}
|
||||
return {
|
||||
vHref: href
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
emitClick(this, arguments)
|
||||
|
||||
if (this.vHref.length > 0) {
|
||||
window.location = this.vHref
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<a :href="vHref" :title="title" style="border-bottom: 1px #db2828 dashed" @click.prevent="clickPrevent"><span class="red"><slot></slot></span></a>`
|
||||
})
|
||||
|
||||
// 会弹出窗口的链接
|
||||
Vue.component("link-popup", {
|
||||
props: ["title"],
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
emitClick(this, arguments)
|
||||
}
|
||||
},
|
||||
template: `<a href="" :title="title" @click.prevent="clickPrevent"><slot></slot></a>`
|
||||
})
|
||||
|
||||
Vue.component("popup-icon", {
|
||||
props: ["title", "href", "height"],
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
if (this.href != null && this.href.length > 0) {
|
||||
teaweb.popup(this.href, {
|
||||
height: this.height
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<span><slot></slot> <a href="" :title="title" @click.prevent="clickPrevent"><i class="icon expand small"></i></a></span>`
|
||||
})
|
||||
|
||||
// 小提示
|
||||
Vue.component("tip-icon", {
|
||||
props: ["content"],
|
||||
methods: {
|
||||
showTip: function () {
|
||||
teaweb.popupTip(this.content)
|
||||
}
|
||||
},
|
||||
template: `<a href="" title="查看帮助" @click.prevent="showTip"><i class="icon question circle grey"></i></a>`
|
||||
})
|
||||
|
||||
// 提交点击事件
|
||||
function emitClick(obj, arguments) {
|
||||
let event = "click"
|
||||
let newArgs = [event]
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
newArgs.push(arguments[i])
|
||||
}
|
||||
obj.$emit.apply(obj, newArgs)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Vue.component("mask-warning", {
|
||||
template: `<span class="red">为了安全起见,此项数据保存后将不允许在界面查看完整明文,为避免忘记,请自行记录原始数据。</span>`
|
||||
})
|
||||
45
EdgeAdmin/web/public/js/components/common/menu-item.js
Normal file
45
EdgeAdmin/web/public/js/components/common/menu-item.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 菜单项
|
||||
*/
|
||||
Vue.component("menu-item", {
|
||||
props: ["href", "active", "code"],
|
||||
data: function () {
|
||||
let active = this.active
|
||||
if (typeof (active) == "undefined") {
|
||||
var itemCode = ""
|
||||
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
|
||||
itemCode = window.TEA.ACTION.data.firstMenuItem
|
||||
}
|
||||
if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) {
|
||||
if (itemCode.indexOf(",") > 0) {
|
||||
active = itemCode.split(",").$contains(this.code)
|
||||
} else {
|
||||
active = (itemCode == this.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let href = (this.href == null) ? "" : this.href
|
||||
if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) {
|
||||
let qIndex = href.indexOf("?")
|
||||
if (qIndex >= 0) {
|
||||
href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex)
|
||||
} else {
|
||||
href = Tea.url(href)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vHref: href,
|
||||
vActive: active
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click: function (e) {
|
||||
this.$emit("click", e)
|
||||
}
|
||||
},
|
||||
template: '\
|
||||
<a :href="vHref" class="item" :class="{active:vActive}" @click="click"><slot></slot></a> \
|
||||
'
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
Vue.component("loading-message", {
|
||||
template: `<div class="ui message loading">
|
||||
<div class="ui active inline loader small"></div> <slot></slot>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
// 可以展示更多条目的角图表
|
||||
Vue.component("more-items-angle", {
|
||||
props: ["v-data-url", "v-url"],
|
||||
data: function () {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function () {
|
||||
this.visible = !this.visible
|
||||
if (this.visible) {
|
||||
this.showBox()
|
||||
} else {
|
||||
this.hideBox()
|
||||
}
|
||||
},
|
||||
showBox: function () {
|
||||
let that = this
|
||||
|
||||
this.visible = true
|
||||
|
||||
Tea.action(this.vDataUrl)
|
||||
.params({
|
||||
url: this.vUrl
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
let groups = resp.data.groups
|
||||
|
||||
let boxLeft = that.$el.offsetLeft + 120;
|
||||
let boxTop = that.$el.offsetTop + 70;
|
||||
|
||||
let box = document.createElement("div")
|
||||
box.setAttribute("id", "more-items-box")
|
||||
box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)"
|
||||
document.body.append(box)
|
||||
|
||||
let menuHTML = "<ul class=\"ui labeled menu vertical borderless\" style=\"padding: 0\">"
|
||||
groups.forEach(function (group) {
|
||||
menuHTML += "<div class=\"item header\">" + teaweb.encodeHTML(group.name) + "</div>"
|
||||
group.items.forEach(function (item) {
|
||||
menuHTML += "<a href=\"" + item.url + "\" class=\"item " + (item.isActive ? "active" : "") + "\" style=\"font-size: 0.9em;\">" + teaweb.encodeHTML(item.name) + "<i class=\"icon right angle\"></i></a>"
|
||||
})
|
||||
})
|
||||
menuHTML += "</ul>"
|
||||
box.innerHTML = menuHTML
|
||||
|
||||
let listener = function (e) {
|
||||
if (e.target.tagName == "I") {
|
||||
return
|
||||
}
|
||||
|
||||
if (!that.isInBox(box, e.target)) {
|
||||
document.removeEventListener("click", listener)
|
||||
that.hideBox()
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", listener)
|
||||
})
|
||||
},
|
||||
hideBox: function () {
|
||||
let box = document.getElementById("more-items-box")
|
||||
if (box != null) {
|
||||
box.parentNode.removeChild(box)
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
isInBox: function (parent, child) {
|
||||
while (true) {
|
||||
if (child == null) {
|
||||
break
|
||||
}
|
||||
if (child.parentNode == parent) {
|
||||
return true
|
||||
}
|
||||
child = child.parentNode
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
template: `<a href="" class="item" @click.prevent="show" style="padding-right: 0"><span style="font-size: 0.8em">切换</span><i class="icon angle" :class="{down: !visible, up: visible}"></i></a>`
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
Vue.component("more-options-angle", {
|
||||
data: function () {
|
||||
return {
|
||||
isVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function () {
|
||||
this.isVisible = !this.isVisible
|
||||
this.$emit("change", this.isVisible)
|
||||
}
|
||||
},
|
||||
template: `<a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="icon angle" :class="{down:!isVisible, up:isVisible}"></i></a>`
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 更多选项
|
||||
*/
|
||||
Vue.component("more-options-indicator", {
|
||||
props:[],
|
||||
data: function () {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeVisible: function () {
|
||||
this.visible = !this.visible
|
||||
if (Tea.Vue != null) {
|
||||
Tea.Vue.moreOptionsVisible = this.visible
|
||||
}
|
||||
this.$emit("change", this.visible)
|
||||
this.$emit("input", this.visible)
|
||||
}
|
||||
},
|
||||
template: '<a href="" style="font-weight: normal" @click.prevent="changeVisible()"><slot><span v-if="!visible">更多选项</span><span v-if="visible">收起选项</span></slot> <i class="icon angle" :class="{down:!visible, up:visible}"></i> </a>'
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
Vue.component("more-options-tbody", {
|
||||
data: function () {
|
||||
return {
|
||||
isVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function () {
|
||||
this.isVisible = !this.isVisible
|
||||
this.$emit("change", this.isVisible)
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="icon angle" :class="{down:!isVisible, up:isVisible}"></i></a></td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
Vue.component("network-addresses-box", {
|
||||
props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"],
|
||||
data: function () {
|
||||
let addresses = this.vAddresses
|
||||
if (addresses == null) {
|
||||
addresses = []
|
||||
}
|
||||
let protocol = this.vProtocol
|
||||
if (protocol == null) {
|
||||
protocol = ""
|
||||
}
|
||||
|
||||
let name = this.vName
|
||||
if (name == null) {
|
||||
name = "addresses"
|
||||
}
|
||||
|
||||
let from = this.vFrom
|
||||
if (from == null) {
|
||||
from = ""
|
||||
}
|
||||
|
||||
return {
|
||||
addresses: addresses,
|
||||
protocol: protocol,
|
||||
name: name,
|
||||
from: from,
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"vServerType": function () {
|
||||
this.addresses = []
|
||||
},
|
||||
"vAddresses": function () {
|
||||
if (this.vAddresses != null) {
|
||||
this.addresses = this.vAddresses
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAddr: function () {
|
||||
this.isEditing = true
|
||||
|
||||
let that = this
|
||||
window.UPDATING_ADDR = null
|
||||
|
||||
let url = this.vUrl
|
||||
if (url == null) {
|
||||
url = "/servers/addPortPopup"
|
||||
}
|
||||
|
||||
teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), {
|
||||
height: "18em",
|
||||
callback: function (resp) {
|
||||
var addr = resp.data.address
|
||||
if (that.addresses.$find(function (k, v) {
|
||||
return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol
|
||||
}) != null) {
|
||||
teaweb.warn("要添加的网络地址已经存在")
|
||||
return
|
||||
}
|
||||
that.addresses.push(addr)
|
||||
if (["https", "https4", "https6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "HTTPS"
|
||||
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "TLS"
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
that.$emit("change", that.addresses)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeAddr: function (index) {
|
||||
this.addresses.$remove(index);
|
||||
|
||||
// 发送事件
|
||||
this.$emit("change", this.addresses)
|
||||
},
|
||||
updateAddr: function (index, addr) {
|
||||
let that = this
|
||||
window.UPDATING_ADDR = addr
|
||||
|
||||
let url = this.vUrl
|
||||
if (url == null) {
|
||||
url = "/servers/addPortPopup"
|
||||
}
|
||||
|
||||
teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), {
|
||||
height: "18em",
|
||||
callback: function (resp) {
|
||||
var addr = resp.data.address
|
||||
Vue.set(that.addresses, index, addr)
|
||||
|
||||
if (["https", "https4", "https6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "HTTPS"
|
||||
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "TLS"
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
that.$emit("change", that.addresses)
|
||||
}
|
||||
})
|
||||
|
||||
// 发送事件
|
||||
this.$emit("change", this.addresses)
|
||||
},
|
||||
supportRange: function () {
|
||||
return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy")
|
||||
},
|
||||
edit: function () {
|
||||
this.isEditing = true
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="name" :value="JSON.stringify(addresses)"/>
|
||||
<div v-show="!isEditing">
|
||||
<div v-if="addresses.length > 0">
|
||||
<div class="ui label small basic" v-for="(addr, index) in addresses">
|
||||
{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host.quoteIP()}}</span><span v-if="addr.host.length == 0">*</span>:<span v-if="addr.portRange.indexOf('-')<0">{{addr.portRange}}</span><span v-else style="font-style: italic">{{addr.portRange}}</span>
|
||||
</div>
|
||||
<a href="" @click.prevent="edit" style="font-size: 0.9em">[修改]</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isEditing || addresses.length == 0">
|
||||
<div v-if="addresses.length > 0">
|
||||
<div class="ui label small basic" v-for="(addr, index) in addresses">
|
||||
{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host.quoteIP()}}</span><span v-if="addr.host.length == 0">*</span>:<span v-if="addr.portRange.indexOf('-')<0">{{addr.portRange}}</span><span v-else style="font-style: italic">{{addr.portRange}}</span>
|
||||
<a href="" @click.prevent="updateAddr(index, addr)" title="修改"><i class="icon pencil small"></i></a>
|
||||
<a href="" @click.prevent="removeAddr(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<a href="" @click.prevent="addAddr()">[添加端口绑定]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
Vue.component("network-addresses-view", {
|
||||
props: ["v-addresses"],
|
||||
template: `<div>
|
||||
<div class="ui label tiny basic" v-if="vAddresses != null" v-for="addr in vAddresses">
|
||||
{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host.quoteIP()}}</span><span v-else>*</span>:{{addr.portRange}}
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
12
EdgeAdmin/web/public/js/components/common/node-log-row.js
Normal file
12
EdgeAdmin/web/public/js/components/common/node-log-row.js
Normal file
@@ -0,0 +1,12 @@
|
||||
Vue.component("node-log-row", {
|
||||
props: ["v-log", "v-keyword"],
|
||||
data: function () {
|
||||
return {
|
||||
log: this.vLog,
|
||||
keyword: this.vKeyword
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<pre class="log-box" style="margin: 0; padding: 0"><span :class="{red:log.level == 'error', orange:log.level == 'warning', green: log.level == 'success'}"><span v-if="!log.isToday">[{{log.createdTime}}]</span><strong v-if="log.isToday">[{{log.createdTime}}]</strong><keyword :v-word="keyword">[{{log.tag}}]{{log.description}}</keyword></span> <span v-if="log.count > 1" class="ui label tiny" :class="{red:log.level == 'error', orange:log.level == 'warning'}">共{{log.count}}条</span> <span v-if="log.server != null && log.server.id > 0"><a :href="'/servers/server?serverId=' + log.server.id" class="ui label tiny basic">{{log.server.name}}</a></span></pre>
|
||||
</div>`
|
||||
})
|
||||
37
EdgeAdmin/web/public/js/components/common/node-role-name.js
Normal file
37
EdgeAdmin/web/public/js/components/common/node-role-name.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// 节点角色名称
|
||||
Vue.component("node-role-name", {
|
||||
props: ["v-role"],
|
||||
data: function () {
|
||||
let roleName = ""
|
||||
switch (this.vRole) {
|
||||
case "node":
|
||||
roleName = "边缘节点"
|
||||
break
|
||||
case "monitor":
|
||||
roleName = "监控节点"
|
||||
break
|
||||
case "api":
|
||||
roleName = "API节点"
|
||||
break
|
||||
case "user":
|
||||
roleName = "用户平台"
|
||||
break
|
||||
case "admin":
|
||||
roleName = "管理平台"
|
||||
break
|
||||
case "database":
|
||||
roleName = "数据库节点"
|
||||
break
|
||||
case "dns":
|
||||
roleName = "DNS节点"
|
||||
break
|
||||
case "report":
|
||||
roleName = "区域监控终端"
|
||||
break
|
||||
}
|
||||
return {
|
||||
roleName: roleName
|
||||
}
|
||||
},
|
||||
template: `<span>{{roleName}}</span>`
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
Vue.component("not-found-box", {
|
||||
props: ["message"],
|
||||
template: `<div style="text-align: center; margin-top: 5em;">
|
||||
<div style="font-size: 2em; margin-bottom: 1em"><i class="icon exclamation triangle large grey"></i></div>
|
||||
<p class="comment">{{message}}<slot></slot></p>
|
||||
</div>`
|
||||
})
|
||||
16
EdgeAdmin/web/public/js/components/common/page-box.js
Normal file
16
EdgeAdmin/web/public/js/components/common/page-box.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Vue.component("page-box", {
|
||||
data: function () {
|
||||
return {
|
||||
page: ""
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
let that = this;
|
||||
setTimeout(function () {
|
||||
that.page = Tea.Vue.page;
|
||||
})
|
||||
},
|
||||
template: `<div>
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
Vue.component("page-size-selector", {
|
||||
data: function () {
|
||||
let query = window.location.search
|
||||
let pageSize = 10
|
||||
if (query.length > 0) {
|
||||
query = query.substr(1)
|
||||
let params = query.split("&")
|
||||
params.forEach(function (v) {
|
||||
let pieces = v.split("=")
|
||||
if (pieces.length == 2 && pieces[0] == "pageSize") {
|
||||
let pageSizeString = pieces[1]
|
||||
if (pageSizeString.match(/^\d+$/)) {
|
||||
pageSize = parseInt(pageSizeString, 10)
|
||||
if (isNaN(pageSize) || pageSize < 1) {
|
||||
pageSize = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
pageSize: pageSize
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pageSize: function () {
|
||||
window.ChangePageSize(this.pageSize)
|
||||
}
|
||||
},
|
||||
template: `<select class="ui dropdown" style="height:34px;padding-top:0;padding-bottom:0;margin-left:1em;color:#666" v-model="pageSize">
|
||||
\t<option value="10">[每页]</option><option value="10" selected="selected">10条</option><option value="20">20条</option><option value="30">30条</option><option value="40">40条</option><option value="50">50条</option><option value="60">60条</option><option value="70">70条</option><option value="80">80条</option><option value="90">90条</option><option value="100">100条</option>
|
||||
</select>`
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
Vue.component("provinces-selector", {
|
||||
props: ["v-provinces"],
|
||||
data: function () {
|
||||
let provinces = this.vProvinces
|
||||
if (provinces == null) {
|
||||
provinces = []
|
||||
}
|
||||
let provinceIds = provinces.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
return {
|
||||
provinces: provinces,
|
||||
provinceIds: provinceIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
let provinceStringIds = this.provinceIds.map(function (v) {
|
||||
return v.toString()
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), {
|
||||
width: "48em",
|
||||
height: "23em",
|
||||
callback: function (resp) {
|
||||
that.provinces = resp.data.provinces
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.provinces.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.provinceIds = this.provinces.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="provinceIdsJSON" :value="JSON.stringify(provinceIds)"/>
|
||||
<div v-if="provinces.length > 0" style="margin-bottom: 0.5em">
|
||||
<div v-for="(province, index) in provinces" class="ui label tiny basic">{{province.name}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
23
EdgeAdmin/web/public/js/components/common/radio.js
Normal file
23
EdgeAdmin/web/public/js/components/common/radio.js
Normal file
@@ -0,0 +1,23 @@
|
||||
let radioId = 0
|
||||
Vue.component("radio", {
|
||||
props: ["name", "value", "v-value", "id"],
|
||||
data: function () {
|
||||
radioId++
|
||||
let elementId = this.id
|
||||
if (elementId == null) {
|
||||
elementId = "radio" + radioId
|
||||
}
|
||||
return {
|
||||
"elementId": elementId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.vValue)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui checkbox radio">
|
||||
<input type="radio" :name="name" :value="vValue" :id="elementId" @change="change" :checked="(vValue == value)"/>
|
||||
<label :for="elementId"><slot></slot></label>
|
||||
</div>`
|
||||
})
|
||||
3
EdgeAdmin/web/public/js/components/common/raquo.js
Normal file
3
EdgeAdmin/web/public/js/components/common/raquo.js
Normal file
@@ -0,0 +1,3 @@
|
||||
Vue.component("raquo-item", {
|
||||
template: `<span class="item disabled" style="padding: 0">»</span>`
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
// 将变量转换为中文
|
||||
Vue.component("request-variables-describer", {
|
||||
data: function () {
|
||||
return {
|
||||
vars:[]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function (variablesString) {
|
||||
this.vars = []
|
||||
let that = this
|
||||
variablesString.replace(/\${.+?}/g, function (v) {
|
||||
let def = that.findVar(v)
|
||||
if (def == null) {
|
||||
return v
|
||||
}
|
||||
that.vars.push(def)
|
||||
})
|
||||
},
|
||||
findVar: function (name) {
|
||||
let def = null
|
||||
window.REQUEST_VARIABLES.forEach(function (v) {
|
||||
if (v.code == name) {
|
||||
def = v
|
||||
}
|
||||
})
|
||||
return def
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
<span v-for="(v, index) in vars"><code-label :title="v.description">{{v.code}}</code-label> - {{v.name}}<span v-if="index < vars.length-1">;</span></span>
|
||||
</span>`
|
||||
})
|
||||
33
EdgeAdmin/web/public/js/components/common/search-box.js
Normal file
33
EdgeAdmin/web/public/js/components/common/search-box.js
Normal file
@@ -0,0 +1,33 @@
|
||||
Vue.component("search-box", {
|
||||
props: ["placeholder", "width"],
|
||||
data: function () {
|
||||
let width = this.width
|
||||
if (width == null) {
|
||||
width = "10em"
|
||||
}
|
||||
return {
|
||||
realWidth: width,
|
||||
realValue: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput: function () {
|
||||
this.$emit("input", { value: this.realValue})
|
||||
this.$emit("change", { value: this.realValue})
|
||||
},
|
||||
clearValue: function () {
|
||||
this.realValue = ""
|
||||
this.focus()
|
||||
this.onInput()
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.valueRef.focus()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui input small" :class="{'right labeled': realValue.length > 0}">
|
||||
<input type="text" :placeholder="placeholder" :style="{width: realWidth}" @input="onInput" v-model="realValue" ref="valueRef"/>
|
||||
<a href="" class="ui label blue" v-if="realValue.length > 0" @click.prevent="clearValue" style="padding-right: 0"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
12
EdgeAdmin/web/public/js/components/common/second-menu.js
Normal file
12
EdgeAdmin/web/public/js/components/common/second-menu.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 二级菜单
|
||||
*/
|
||||
Vue.component("second-menu", {
|
||||
template: ' \
|
||||
<div class="second-menu"> \
|
||||
<div class="ui menu text blue small">\
|
||||
<slot></slot>\
|
||||
</div> \
|
||||
<div class="ui divider"></div> \
|
||||
</div>'
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
Vue.component("size-capacity-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let vSize = this.size
|
||||
if (vSize == null) {
|
||||
vSize = 6
|
||||
}
|
||||
|
||||
let vMaxlength = this.maxlength
|
||||
if (vMaxlength == null) {
|
||||
vMaxlength = 10
|
||||
}
|
||||
|
||||
let supportedUnits = this.vSupportedUnits
|
||||
if (supportedUnits == null) {
|
||||
supportedUnits = []
|
||||
}
|
||||
|
||||
return {
|
||||
capacity: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
vSize: vSize,
|
||||
vMaxlength: vMaxlength,
|
||||
supportedUnits: supportedUnits
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.capacity.count = -1
|
||||
this.change()
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.capacity.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.capacity)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="capacity.unit" @change="change">
|
||||
<option value="byte" v-if="supportedUnits.length == 0 || supportedUnits.$contains('byte')">字节</option>
|
||||
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">KiB</option>
|
||||
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">MiB</option>
|
||||
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">GiB</option>
|
||||
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">TiB</option>
|
||||
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">PiB</option>
|
||||
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">EiB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
Vue.component("size-capacity-view", {
|
||||
props:["v-default-text", "v-value"],
|
||||
methods: {
|
||||
composeCapacity: function (capacity) {
|
||||
return teaweb.convertSizeCapacityToString(capacity)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="vValue != null && vValue.count > 0">{{composeCapacity(vValue)}}</span>
|
||||
<span v-else>{{vDefaultText}}</span>
|
||||
</div>`
|
||||
})
|
||||
53
EdgeAdmin/web/public/js/components/common/sort-arrow.js
Normal file
53
EdgeAdmin/web/public/js/components/common/sort-arrow.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// 排序使用的箭头
|
||||
Vue.component("sort-arrow", {
|
||||
props: ["name"],
|
||||
data: function () {
|
||||
let url = window.location.toString()
|
||||
let order = ""
|
||||
let iconTitle = ""
|
||||
let newArgs = []
|
||||
if (window.location.search != null && window.location.search.length > 0) {
|
||||
let queryString = window.location.search.substring(1)
|
||||
let pieces = queryString.split("&")
|
||||
let that = this
|
||||
pieces.forEach(function (v) {
|
||||
let eqIndex = v.indexOf("=")
|
||||
if (eqIndex > 0) {
|
||||
let argName = v.substring(0, eqIndex)
|
||||
let argValue = v.substring(eqIndex + 1)
|
||||
if (argName == that.name) {
|
||||
order = argValue
|
||||
} else if (argName != "page" && argValue != "asc" && argValue != "desc") {
|
||||
newArgs.push(v)
|
||||
}
|
||||
} else {
|
||||
newArgs.push(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (order == "asc") {
|
||||
newArgs.push(this.name + "=desc")
|
||||
iconTitle = "当前正序排列"
|
||||
} else if (order == "desc") {
|
||||
newArgs.push(this.name + "=asc")
|
||||
iconTitle = "当前倒序排列"
|
||||
} else {
|
||||
newArgs.push(this.name + "=desc")
|
||||
iconTitle = "当前正序排列"
|
||||
}
|
||||
|
||||
let qIndex = url.indexOf("?")
|
||||
if (qIndex > 0) {
|
||||
url = url.substring(0, qIndex) + "?" + newArgs.join("&")
|
||||
} else {
|
||||
url = url + "?" + newArgs.join("&")
|
||||
}
|
||||
|
||||
return {
|
||||
order: order,
|
||||
url: url,
|
||||
iconTitle: iconTitle
|
||||
}
|
||||
},
|
||||
template: `<a :href="url" :title="iconTitle"> <i class="ui icon long arrow small" :class="{down: order == 'asc', up: order == 'desc', 'down grey': order == '' || order == null}"></i></a>`
|
||||
})
|
||||
39
EdgeAdmin/web/public/js/components/common/sortable.js
Normal file
39
EdgeAdmin/web/public/js/components/common/sortable.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// 给Table增加排序功能
|
||||
function sortTable(callback) {
|
||||
// 引入js
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/sortable.min.js")
|
||||
jsFile.addEventListener("load", function () {
|
||||
// 初始化
|
||||
let box = document.querySelector("#sortable-table")
|
||||
if (box == null) {
|
||||
return
|
||||
}
|
||||
Sortable.create(box, {
|
||||
draggable: "tbody",
|
||||
handle: ".icon.handle",
|
||||
onStart: function () {
|
||||
},
|
||||
onUpdate: function (event) {
|
||||
let rows = box.querySelectorAll("tbody")
|
||||
let rowIds = []
|
||||
rows.forEach(function (row) {
|
||||
rowIds.push(parseInt(row.getAttribute("v-id")))
|
||||
})
|
||||
callback(rowIds)
|
||||
}
|
||||
})
|
||||
})
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
|
||||
function sortLoad(callback) {
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/sortable.min.js")
|
||||
jsFile.addEventListener("load", function () {
|
||||
if (typeof (callback) == "function") {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
93
EdgeAdmin/web/public/js/components/common/source-code-box.js
Normal file
93
EdgeAdmin/web/public/js/components/common/source-code-box.js
Normal file
@@ -0,0 +1,93 @@
|
||||
let sourceCodeBoxIndex = 0
|
||||
|
||||
Vue.component("source-code-box", {
|
||||
props: ["name", "type", "id", "read-only", "width", "height", "focus"],
|
||||
mounted: function () {
|
||||
let readOnly = this.readOnly
|
||||
if (typeof readOnly != "boolean") {
|
||||
readOnly = true
|
||||
}
|
||||
let box = document.getElementById("source-code-box-" + this.index)
|
||||
let valueBox = document.getElementById(this.valueBoxId)
|
||||
let value = ""
|
||||
if (valueBox.textContent != null) {
|
||||
value = valueBox.textContent
|
||||
} else if (valueBox.innerText != null) {
|
||||
value = valueBox.innerText
|
||||
}
|
||||
|
||||
this.createEditor(box, value, readOnly)
|
||||
},
|
||||
data: function () {
|
||||
let index = sourceCodeBoxIndex++
|
||||
|
||||
let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex
|
||||
if (this.id != null) {
|
||||
valueBoxId = this.id
|
||||
}
|
||||
|
||||
return {
|
||||
index: index,
|
||||
valueBoxId: valueBoxId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createEditor: function (box, value, readOnly) {
|
||||
let boxEditor = CodeMirror.fromTextArea(box, {
|
||||
theme: "idea",
|
||||
lineNumbers: true,
|
||||
value: "",
|
||||
readOnly: readOnly,
|
||||
showCursorWhenSelecting: true,
|
||||
height: "auto",
|
||||
//scrollbarStyle: null,
|
||||
viewportMargin: Infinity,
|
||||
lineWrapping: true,
|
||||
highlightFormatting: false,
|
||||
indentUnit: 4,
|
||||
indentWithTabs: true,
|
||||
})
|
||||
let that = this
|
||||
boxEditor.on("change", function () {
|
||||
that.change(boxEditor.getValue())
|
||||
})
|
||||
boxEditor.setValue(value)
|
||||
|
||||
if (this.focus) {
|
||||
boxEditor.focus()
|
||||
}
|
||||
|
||||
let width = this.width
|
||||
let height = this.height
|
||||
if (width != null && height != null) {
|
||||
width = parseInt(width)
|
||||
height = parseInt(height)
|
||||
if (!isNaN(width) && !isNaN(height)) {
|
||||
if (width <= 0) {
|
||||
width = box.parentNode.offsetWidth
|
||||
}
|
||||
boxEditor.setSize(width, height)
|
||||
}
|
||||
} else if (height != null) {
|
||||
height = parseInt(height)
|
||||
if (!isNaN(height)) {
|
||||
boxEditor.setSize("100%", height)
|
||||
}
|
||||
}
|
||||
|
||||
let info = CodeMirror.findModeByMIME(this.type)
|
||||
if (info != null) {
|
||||
boxEditor.setOption("mode", info.mode)
|
||||
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
|
||||
CodeMirror.autoLoadMode(boxEditor, info.mode)
|
||||
}
|
||||
},
|
||||
change: function (code) {
|
||||
this.$emit("change", code)
|
||||
}
|
||||
},
|
||||
template: `<div class="source-code-box">
|
||||
<div style="display: none" :id="valueBoxId"><slot></slot></div>
|
||||
<textarea :id="'source-code-box-' + index" :name="name"></textarea>
|
||||
</div>`
|
||||
})
|
||||
6
EdgeAdmin/web/public/js/components/common/submit-btn.js
Normal file
6
EdgeAdmin/web/public/js/components/common/submit-btn.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 保存按钮
|
||||
*/
|
||||
Vue.component("submit-btn", {
|
||||
template: '<button class="ui button primary" type="submit"><slot>保存</slot></button>'
|
||||
});
|
||||
119
EdgeAdmin/web/public/js/components/common/time-duration-box.js
Normal file
119
EdgeAdmin/web/public/js/components/common/time-duration-box.js
Normal file
@@ -0,0 +1,119 @@
|
||||
Vue.component("time-duration-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"],
|
||||
mounted: function () {
|
||||
this.change()
|
||||
},
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let minUnit = this.vMinUnit
|
||||
let units = [
|
||||
{
|
||||
code: "ms",
|
||||
name: "毫秒"
|
||||
},
|
||||
{
|
||||
code: "second",
|
||||
name: "秒"
|
||||
},
|
||||
{
|
||||
code: "minute",
|
||||
name: "分钟"
|
||||
},
|
||||
{
|
||||
code: "hour",
|
||||
name: "小时"
|
||||
},
|
||||
{
|
||||
code: "day",
|
||||
name: "天"
|
||||
}
|
||||
]
|
||||
let minUnitIndex = -1
|
||||
if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) {
|
||||
for (let i = 0; i < units.length; i++) {
|
||||
if (units[i].code == minUnit) {
|
||||
minUnitIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minUnitIndex > -1) {
|
||||
units = units.slice(minUnitIndex)
|
||||
}
|
||||
|
||||
let maxLength = parseInt(this.maxlength)
|
||||
if (typeof maxLength != "number") {
|
||||
maxLength = 10
|
||||
}
|
||||
|
||||
return {
|
||||
duration: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
units: units,
|
||||
realMaxLength: maxLength
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.duration.count = -1
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.duration.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.duration)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline" style="padding-bottom: 0; margin-bottom: 0">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(duration)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="realMaxLength" :size="realMaxLength" :placeholder="placeholder" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="duration.unit" @change="change">
|
||||
<option v-for="unit in units" :value="unit.code">{{unit.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("time-duration-text", {
|
||||
props: ["v-value"],
|
||||
methods: {
|
||||
unitName: function (unit) {
|
||||
switch (unit) {
|
||||
case "ms":
|
||||
return "毫秒"
|
||||
case "second":
|
||||
return "秒"
|
||||
case "minute":
|
||||
return "分钟"
|
||||
case "hour":
|
||||
return "小时"
|
||||
case "day":
|
||||
return "天"
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
{{vValue.count}} {{unitName(vValue.unit)}}
|
||||
</span>`
|
||||
})
|
||||
37
EdgeAdmin/web/public/js/components/common/tip-message-box.js
Normal file
37
EdgeAdmin/web/public/js/components/common/tip-message-box.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// 信息提示窗口
|
||||
Vue.component("tip-message-box", {
|
||||
props: ["code"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/ui/showTip")
|
||||
.params({
|
||||
code: this.code
|
||||
})
|
||||
.success(function (resp) {
|
||||
that.visible = resp.data.visible
|
||||
})
|
||||
.post()
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close: function () {
|
||||
this.visible = false
|
||||
Tea.action("/ui/hideTip")
|
||||
.params({
|
||||
code: this.code
|
||||
})
|
||||
.post()
|
||||
}
|
||||
},
|
||||
template: `<div class="ui icon message" v-if="visible">
|
||||
<i class="icon info circle"></i>
|
||||
<i class="close icon" title="取消" @click.prevent="close" style="margin-top: 1em"></i>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
149
EdgeAdmin/web/public/js/components/common/url-patterns-box.js
Normal file
149
EdgeAdmin/web/public/js/components/common/url-patterns-box.js
Normal file
@@ -0,0 +1,149 @@
|
||||
Vue.component("url-patterns-box", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let patterns = []
|
||||
if (this.value != null) {
|
||||
patterns = this.value
|
||||
}
|
||||
|
||||
return {
|
||||
patterns: patterns,
|
||||
isAdding: false,
|
||||
|
||||
addingPattern: {"type": "wildcard", "pattern": ""},
|
||||
editingIndex: -1,
|
||||
|
||||
patternIsInvalid: false,
|
||||
|
||||
windowIsSmall: window.innerWidth < 600
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.patternInput.focus()
|
||||
})
|
||||
},
|
||||
edit: function (index) {
|
||||
this.isAdding = true
|
||||
this.editingIndex = index
|
||||
this.addingPattern = {
|
||||
type: this.patterns[index].type,
|
||||
pattern: this.patterns[index].pattern
|
||||
}
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.requireURL(this.addingPattern.type)) {
|
||||
let pattern = this.addingPattern.pattern.trim()
|
||||
if (pattern.length == 0) {
|
||||
let that = this
|
||||
teaweb.warn("请输入URL", function () {
|
||||
that.$refs.patternInput.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.editingIndex < 0) {
|
||||
this.patterns.push({
|
||||
type: this.addingPattern.type,
|
||||
pattern: this.addingPattern.pattern
|
||||
})
|
||||
} else {
|
||||
this.patterns[this.editingIndex].type = this.addingPattern.type
|
||||
this.patterns[this.editingIndex].pattern = this.addingPattern.pattern
|
||||
}
|
||||
this.notifyChange()
|
||||
this.cancel()
|
||||
},
|
||||
remove: function (index) {
|
||||
this.patterns.$remove(index)
|
||||
this.cancel()
|
||||
this.notifyChange()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingPattern = {"type": "wildcard", "pattern": ""}
|
||||
this.editingIndex = -1
|
||||
},
|
||||
patternTypeName: function (patternType) {
|
||||
switch (patternType) {
|
||||
case "wildcard":
|
||||
return "通配符"
|
||||
case "regexp":
|
||||
return "正则"
|
||||
case "images":
|
||||
return "常见图片文件"
|
||||
case "audios":
|
||||
return "常见音频文件"
|
||||
case "videos":
|
||||
return "常见视频文件"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("input", this.patterns)
|
||||
},
|
||||
changePattern: function () {
|
||||
this.patternIsInvalid = false
|
||||
let pattern = this.addingPattern.pattern
|
||||
switch (this.addingPattern.type) {
|
||||
case "wildcard":
|
||||
if (pattern.indexOf("?") >= 0) {
|
||||
this.patternIsInvalid = true
|
||||
}
|
||||
break
|
||||
case "regexp":
|
||||
if (pattern.indexOf("?") >= 0) {
|
||||
let pieces = pattern.split("?")
|
||||
for (let i = 0; i < pieces.length - 1; i++) {
|
||||
if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") {
|
||||
this.patternIsInvalid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
requireURL: function (patternType) {
|
||||
return patternType == "wildcard" || patternType == "regexp"
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-show="patterns.length > 0">
|
||||
<div v-for="(pattern, index) in patterns" class="ui label basic small" :class="{blue: index == editingIndex, disabled: isAdding && index != editingIndex}" style="margin-bottom: 0.8em">
|
||||
<span class="grey" style="font-weight: normal">[{{patternTypeName(pattern.type)}}]</span> <span >{{pattern.pattern}}</span>
|
||||
<a href="" title="修改" @click.prevent="edit(index)"><i class="icon pencil tiny"></i></a>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isAdding" style="margin-top: 0.5em">
|
||||
<div :class="{'ui fields inline': !windowIsSmall}">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown auto-width" v-model="addingPattern.type">
|
||||
<option value="wildcard">通配符</option>
|
||||
<option value="regexp">正则表达式</option>
|
||||
<option value="images">常见图片</option>
|
||||
<option value="audios">常见音频</option>
|
||||
<option value="videos">常见视频</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field" v-show="addingPattern.type == 'wildcard' || addingPattern.type == 'regexp'">
|
||||
<input type="text" :placeholder="(addingPattern.type == 'wildcard') ? '可以使用星号(*)通配符,不区分大小写' : '可以使用正则表达式,不区分大小写'" v-model="addingPattern.pattern" @input="changePattern" size="36" ref="patternInput" @keyup.enter="confirm()" @keypress.enter.prevent="1" spellcheck="false"/>
|
||||
<p class="comment" v-if="patternIsInvalid"><span class="red" style="font-weight: normal"><span v-if="addingPattern.type == 'wildcard'">通配符</span><span v-if="addingPattern.type == 'regexp'">正则表达式</span>中不能包含问号(?)及问号以后的内容。</span></p>
|
||||
</div>
|
||||
<div class="ui field" style="padding-left: 0" v-show="addingPattern.type == 'wildcard' || addingPattern.type == 'regexp'">
|
||||
<tip-icon content="通配符示例:<br/>单个路径开头:/hello/world/*<br/>单个路径结尾:*/hello/world<br/>包含某个路径:*/article/*<br/>某个域名下的所有URL:*example.com/*<br/>忽略某个扩展名:*.js" v-if="addingPattern.type == 'wildcard'"></tip-icon>
|
||||
<tip-icon content="正则表达式示例:<br/>单个路径开头:^/hello/world<br/>单个路径结尾:/hello/world$<br/>包含某个路径:/article/<br/>匹配某个数字路径:/article/(\\d+)<br/>某个域名下的所有URL:^(http|https)://example.com/" v-if="addingPattern.type == 'regexp'"></tip-icon>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" :class="{disabled:this.patternIsInvalid}" type="button" @click.prevent="confirm">确定</button><a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if=!isAdding style="margin-top: 0.5em">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
132
EdgeAdmin/web/public/js/components/common/values-box.js
Normal file
132
EdgeAdmin/web/public/js/components/common/values-box.js
Normal file
@@ -0,0 +1,132 @@
|
||||
Vue.component("values-box", {
|
||||
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"],
|
||||
data: function () {
|
||||
let values = this.values;
|
||||
if (values == null) {
|
||||
values = [];
|
||||
}
|
||||
|
||||
if (this.vValues != null && typeof this.vValues == "object") {
|
||||
values = this.vValues
|
||||
}
|
||||
|
||||
return {
|
||||
"realValues": values,
|
||||
"isUpdating": false,
|
||||
"isAdding": false,
|
||||
"index": 0,
|
||||
"value": "",
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
create: function () {
|
||||
this.isAdding = true;
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.$refs.value.focus();
|
||||
}, 200);
|
||||
},
|
||||
update: function (index) {
|
||||
this.cancel()
|
||||
this.isUpdating = true;
|
||||
this.index = index;
|
||||
this.value = this.realValues[index];
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.$refs.value.focus();
|
||||
}, 200);
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.value.length == 0) {
|
||||
if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validate
|
||||
if (typeof(this.validator) == "function") {
|
||||
let resp = this.validator.call(this, this.value)
|
||||
if (typeof resp == "object") {
|
||||
if (typeof resp.isOk == "boolean" && !resp.isOk) {
|
||||
if (typeof resp.message == "string") {
|
||||
let that = this
|
||||
teaweb.warn(resp.message, function () {
|
||||
that.$refs.value.focus();
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isUpdating) {
|
||||
Vue.set(this.realValues, this.index, this.value);
|
||||
} else {
|
||||
this.realValues.push(this.value);
|
||||
}
|
||||
this.cancel()
|
||||
this.$emit("change", this.realValues)
|
||||
},
|
||||
remove: function (index) {
|
||||
this.realValues.$remove(index)
|
||||
this.$emit("change", this.realValues)
|
||||
},
|
||||
cancel: function () {
|
||||
this.isUpdating = false;
|
||||
this.isAdding = false;
|
||||
this.value = "";
|
||||
},
|
||||
updateAll: function (values) {
|
||||
this.realValues = values
|
||||
},
|
||||
addValue: function (v) {
|
||||
this.realValues.push(v)
|
||||
},
|
||||
|
||||
startEditing: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
},
|
||||
allValues: function () {
|
||||
return this.realValues
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-show="!isEditing && realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
</div>
|
||||
<a href="" @click.prevent="startEditing" style="font-size: 0.8em; margin-left: 0.2em">[修改]</a>
|
||||
</div>
|
||||
<div v-show="isEditing || realValues.length == 0">
|
||||
<div style="margin-bottom: 1em" v-if="realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
<input type="hidden" :name="name" :value="value"/>
|
||||
<a href="" @click.prevent="update(index)" title="修改"><i class="icon pencil small" ></i></a>
|
||||
<a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<!-- 添加|修改 -->
|
||||
<div v-if="isAdding || isUpdating">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" :size="size" :maxlength="maxlength" :placeholder="placeholder" v-model="value" ref="value" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button small" type="button" @click.prevent="confirm()">确定</button>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<a href="" @click.prevent="cancel()" title="取消"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding && !isUpdating">
|
||||
<button class="ui button tiny" type="button" @click.prevent="create()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
// 警告消息
|
||||
Vue.component("warning-message", {
|
||||
template: `<div class="ui icon message warning"><i class="icon warning circle"></i><div class="content"><slot></slot></div></div>`
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
Vue.component("dns-domain-selector", {
|
||||
props: ["v-domain-id", "v-domain-name", "v-provider-name"],
|
||||
data: function () {
|
||||
let domainId = this.vDomainId
|
||||
if (domainId == null) {
|
||||
domainId = 0
|
||||
}
|
||||
let domainName = this.vDomainName
|
||||
if (domainName == null) {
|
||||
domainName = ""
|
||||
}
|
||||
|
||||
let providerName = this.vProviderName
|
||||
if (providerName == null) {
|
||||
providerName = ""
|
||||
}
|
||||
|
||||
return {
|
||||
domainId: domainId,
|
||||
domainName: domainName,
|
||||
providerName: providerName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select: function () {
|
||||
let that = this
|
||||
teaweb.popup("/dns/domains/selectPopup", {
|
||||
callback: function (resp) {
|
||||
that.domainId = resp.data.domainId
|
||||
that.domainName = resp.data.domainName
|
||||
that.providerName = resp.data.providerName
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function() {
|
||||
this.domainId = 0
|
||||
this.domainName = ""
|
||||
this.change()
|
||||
},
|
||||
update: function () {
|
||||
let that = this
|
||||
teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, {
|
||||
callback: function (resp) {
|
||||
that.domainId = resp.data.domainId
|
||||
that.domainName = resp.data.domainName
|
||||
that.providerName = resp.data.providerName
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
change: function () {
|
||||
this.$emit("change", {
|
||||
id: this.domainId,
|
||||
name: this.domainName
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="dnsDomainId" :value="domainId"/>
|
||||
<div v-if="domainName.length > 0">
|
||||
<span class="ui label small basic">
|
||||
<span v-if="providerName != null && providerName.length > 0">{{providerName}} » </span> {{domainName}}
|
||||
<a href="" @click.prevent="update"><i class="icon pencil small"></i></a>
|
||||
<a href="" @click.prevent="remove()"><i class="icon remove"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="domainName.length == 0">
|
||||
<a href="" @click.prevent="select()">[选择域名]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
Vue.component("dns-resolver-config-box", {
|
||||
props:["v-dns-resolver-config"],
|
||||
data: function () {
|
||||
let config = this.vDnsResolverConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
type: "default"
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
types: [
|
||||
{
|
||||
name: "默认",
|
||||
code: "default"
|
||||
},
|
||||
{
|
||||
name: "CGO",
|
||||
code: "cgo"
|
||||
},
|
||||
{
|
||||
name: "Go原生",
|
||||
code: "goNative"
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="dnsResolverJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">使用的DNS解析库</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="config.type">
|
||||
<option v-for="t in types" :value="t.code">{{t.name}}</option>
|
||||
</select>
|
||||
<p class="comment">边缘节点使用的DNS解析库。修改此项配置后,需要重启节点进程才会生效。<pro-warning-label></pro-warning-label></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
Vue.component("dns-resolvers-config-box", {
|
||||
props: ["value", "name"],
|
||||
data: function () {
|
||||
let resolvers = this.value
|
||||
if (resolvers == null) {
|
||||
resolvers = []
|
||||
}
|
||||
|
||||
let name = this.name
|
||||
if (name == null || name.length == 0) {
|
||||
name = "dnsResolversJSON"
|
||||
}
|
||||
|
||||
return {
|
||||
formName: name,
|
||||
resolvers: resolvers,
|
||||
|
||||
host: "",
|
||||
|
||||
isAdding: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.hostRef.focus()
|
||||
})
|
||||
},
|
||||
confirm: function () {
|
||||
let host = this.host.trim()
|
||||
if (host.length == 0) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.hostRef.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
this.resolvers.push({
|
||||
host: host,
|
||||
port: 0, // TODO
|
||||
protocol: "" // TODO
|
||||
})
|
||||
this.cancel()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.host = ""
|
||||
this.port = 0
|
||||
this.protocol = ""
|
||||
},
|
||||
remove: function (index) {
|
||||
this.resolvers.$remove(index)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="formName" :value="JSON.stringify(resolvers)"/>
|
||||
<div v-if="resolvers.length > 0">
|
||||
<div v-for="(resolver, index) in resolvers" class="ui label basic small">
|
||||
<span v-if="resolver.protocol.length > 0">{{resolver.protocol}}</span>{{resolver.host}}<span v-if="resolver.port > 0">:{{resolver.port}}</span>
|
||||
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isAdding" style="margin-top: 1em">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" placeholder="x.x.x.x" @keyup.enter="confirm" @keypress.enter.prevent="1" ref="hostRef" v-model="host"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确认</button>
|
||||
<a href="" @click.prevent="cancel" title="取消"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAdding" style="margin-top: 1em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
132
EdgeAdmin/web/public/js/components/dns/dns-route-selector.js
Normal file
132
EdgeAdmin/web/public/js/components/dns/dns-route-selector.js
Normal file
@@ -0,0 +1,132 @@
|
||||
Vue.component("dns-route-selector", {
|
||||
props: ["v-all-routes", "v-routes"],
|
||||
data: function () {
|
||||
let routes = this.vRoutes
|
||||
if (routes == null) {
|
||||
routes = []
|
||||
}
|
||||
routes.$sort(function (v1, v2) {
|
||||
if (v1.domainId == v2.domainId) {
|
||||
return v1.code < v2.code
|
||||
}
|
||||
return (v1.domainId < v2.domainId) ? 1 : -1
|
||||
})
|
||||
return {
|
||||
routes: routes,
|
||||
routeCodes: routes.$map(function (k, v) {
|
||||
return v.code + "@" + v.domainId
|
||||
}),
|
||||
isAdding: false,
|
||||
routeCode: "",
|
||||
keyword: "",
|
||||
searchingRoutes: this.vAllRoutes.$copy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
this.keyword = ""
|
||||
this.routeCode = ""
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.keywordRef.focus()
|
||||
}, 200)
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.routeCode.length == 0) {
|
||||
return
|
||||
}
|
||||
if (this.routeCodes.$contains(this.routeCode)) {
|
||||
teaweb.warn("已经添加过此线路,不能重复添加")
|
||||
return
|
||||
}
|
||||
let that = this
|
||||
let route = this.vAllRoutes.$find(function (k, v) {
|
||||
return v.code + "@" + v.domainId == that.routeCode
|
||||
})
|
||||
if (route == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.routeCodes.push(this.routeCode)
|
||||
this.routes.push(route)
|
||||
|
||||
this.routes.$sort(function (v1, v2) {
|
||||
if (v1.domainId == v2.domainId) {
|
||||
return v1.code < v2.code
|
||||
}
|
||||
return (v1.domainId < v2.domainId) ? 1 : -1
|
||||
})
|
||||
|
||||
this.routeCode = ""
|
||||
this.isAdding = false
|
||||
},
|
||||
remove: function (route) {
|
||||
this.routeCodes.$removeValue(route.code + "@" + route.domainId)
|
||||
this.routes.$removeIf(function (k, v) {
|
||||
return v.code + "@" + v.domainId == route.code + "@" + route.domainId
|
||||
})
|
||||
},
|
||||
clearKeyword: function () {
|
||||
this.keyword = ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword: function (keyword) {
|
||||
if (keyword.length == 0) {
|
||||
this.searchingRoutes = this.vAllRoutes.$copy()
|
||||
this.routeCode = ""
|
||||
return
|
||||
}
|
||||
this.searchingRoutes = this.vAllRoutes.filter(function (route) {
|
||||
return teaweb.match(route.name, keyword) || teaweb.match(route.code, keyword) || teaweb.match(route.domainName, keyword)
|
||||
})
|
||||
if (this.searchingRoutes.length > 0) {
|
||||
this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId
|
||||
} else {
|
||||
this.routeCode = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="dnsRoutesJSON" :value="JSON.stringify(routeCodes)"/>
|
||||
<div v-if="routes.length > 0">
|
||||
<tiny-basic-label v-for="route in routes" :key="route.code + '@' + route.domainId">
|
||||
{{route.name}} <span class="grey small">({{route.domainName}})</span><a href="" @click.prevent="remove(route)"><i class="icon remove"></i></a>
|
||||
</tiny-basic-label>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button type="button" class="ui button small" @click.prevent="add" v-if="!isAdding">+</button>
|
||||
<div v-if="isAdding">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">所有线路</td>
|
||||
<td>
|
||||
<span v-if="keyword.length > 0 && searchingRoutes.length == 0">没有和关键词“{{keyword}}”匹配的线路</span>
|
||||
<span v-show="keyword.length == 0 || searchingRoutes.length > 0">
|
||||
<select class="ui dropdown" v-model="routeCode">
|
||||
<option value="" v-if="keyword.length == 0">[请选择]</option>
|
||||
<option v-for="route in searchingRoutes" :value="route.code + '@' + route.domainId">{{route.name}}({{route.code}}/{{route.domainName}})</option>
|
||||
</select>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>搜索线路</td>
|
||||
<td>
|
||||
<div class="ui input" :class="{'right labeled':keyword.length > 0}">
|
||||
<input type="text" placeholder="线路名称或代号..." size="10" style="width: 10em" v-model="keyword" ref="keywordRef" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
|
||||
<a class="ui label" v-if="keyword.length > 0" @click.prevent="clearKeyword" href=""><i class="icon remove small blue"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button> <a href="" @click.prevent="cancel()"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
Vue.component("finance-user-selector", {
|
||||
props: ["v-user-id"],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
change: function (userId) {
|
||||
this.$emit("change", userId)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<user-selector :v-user-id="vUserId" data-url="/finance/users/options" @change="change"></user-selector>
|
||||
</div>`
|
||||
})
|
||||
75
EdgeAdmin/web/public/js/components/grant/grant-selector.js
Normal file
75
EdgeAdmin/web/public/js/components/grant/grant-selector.js
Normal file
@@ -0,0 +1,75 @@
|
||||
Vue.component("grant-selector", {
|
||||
props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"],
|
||||
data: function () {
|
||||
return {
|
||||
grantId: (this.vGrant == null) ? 0 : this.vGrant.id,
|
||||
grant: this.vGrant,
|
||||
nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0,
|
||||
nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 选择授权
|
||||
select: function () {
|
||||
let that = this;
|
||||
teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, {
|
||||
callback: (resp) => {
|
||||
that.grantId = resp.data.grant.id;
|
||||
if (that.grantId > 0) {
|
||||
that.grant = resp.data.grant;
|
||||
}
|
||||
that.notifyUpdate()
|
||||
},
|
||||
height: "26em"
|
||||
})
|
||||
},
|
||||
|
||||
// 创建授权
|
||||
create: function () {
|
||||
let that = this
|
||||
teaweb.popup("/clusters/grants/createPopup", {
|
||||
height: "26em",
|
||||
callback: (resp) => {
|
||||
that.grantId = resp.data.grant.id;
|
||||
if (that.grantId > 0) {
|
||||
that.grant = resp.data.grant;
|
||||
}
|
||||
that.notifyUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 修改授权
|
||||
update: function () {
|
||||
if (this.grant == null) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
let that = this
|
||||
teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, {
|
||||
height: "26em",
|
||||
callback: (resp) => {
|
||||
that.grant = resp.data.grant
|
||||
that.notifyUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 删除已选择授权
|
||||
remove: function () {
|
||||
this.grant = null
|
||||
this.grantId = 0
|
||||
this.notifyUpdate()
|
||||
},
|
||||
notifyUpdate: function () {
|
||||
this.$emit("change", this.grant)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="grantId" :value="grantId"/>
|
||||
<div class="ui label small basic" v-if="grant != null">{{grant.name}}<span class="small grey">({{grant.methodName}})</span><span class="small grey" v-if="grant.username != null && grant.username.length > 0">({{grant.username}})</span> <a href="" title="修改" @click.prevent="update()"><i class="icon pencil small"></i></a> <a href="" title="删除" @click.prevent="remove()"><i class="icon remove"></i></a> </div>
|
||||
<div v-if="grant == null">
|
||||
<a href="" @click.prevent="select()">[选择已有认证]</a> <a href="" @click.prevent="create()">[添加新认证]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
21
EdgeAdmin/web/public/js/components/iplist/ip-box.js
Normal file
21
EdgeAdmin/web/public/js/components/iplist/ip-box.js
Normal file
@@ -0,0 +1,21 @@
|
||||
Vue.component("ip-box", {
|
||||
props: ["v-ip"],
|
||||
methods: {
|
||||
popup: function () {
|
||||
let ip = this.vIp
|
||||
if (ip == null || ip.length == 0) {
|
||||
let e = this.$refs.container
|
||||
ip = e.innerText
|
||||
if (ip == null) {
|
||||
ip = e.textContent
|
||||
}
|
||||
}
|
||||
|
||||
teaweb.popup("/servers/ipbox?ip=" + ip, {
|
||||
width: "50em",
|
||||
height: "30em"
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<span @click.prevent="popup()" ref="container"><slot></slot></span>`
|
||||
})
|
||||
14
EdgeAdmin/web/public/js/components/iplist/ip-item-text.js
Normal file
14
EdgeAdmin/web/public/js/components/iplist/ip-item-text.js
Normal file
@@ -0,0 +1,14 @@
|
||||
Vue.component("ip-item-text", {
|
||||
props: ["v-item"],
|
||||
template: `<span>
|
||||
<span v-if="vItem.type == 'all'">*</span>
|
||||
<span v-else>
|
||||
<span v-if="vItem.value != null && vItem.value.length > 0">{{vItem.value}}</span>
|
||||
<span v-else>
|
||||
{{vItem.ipFrom}}
|
||||
<span v-if="vItem.ipTo != null &&vItem.ipTo.length > 0">- {{vItem.ipTo}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="vItem.eventLevelName != null && vItem.eventLevelName.length > 0"> 级别:{{vItem.eventLevelName}}</span>
|
||||
</span>`
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
// 绑定IP列表
|
||||
Vue.component("ip-list-bind-box", {
|
||||
props: ["v-http-firewall-policy-id", "v-type"],
|
||||
mounted: function () {
|
||||
this.refresh()
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
policyId: this.vHttpFirewallPolicyId,
|
||||
type: this.vType,
|
||||
lists: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
bind: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, {
|
||||
width: "50em",
|
||||
height: "34em",
|
||||
callback: function () {
|
||||
|
||||
},
|
||||
onClose: function () {
|
||||
that.refresh()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index, listId) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () {
|
||||
Tea.action("/servers/iplists/unbindHTTPFirewall")
|
||||
.params({
|
||||
httpFirewallPolicyId: that.policyId,
|
||||
listId: listId
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.lists.$remove(index)
|
||||
})
|
||||
})
|
||||
},
|
||||
refresh: function () {
|
||||
let that = this
|
||||
Tea.action("/servers/iplists/httpFirewall")
|
||||
.params({
|
||||
httpFirewallPolicyId: this.policyId,
|
||||
type: this.vType
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.lists = resp.data.lists
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<a href="" @click.prevent="bind()" style="color: rgba(0,0,0,.6)">绑定+</a> <span v-if="lists.length > 0"><span class="disabled small">| </span> 已绑定:</span>
|
||||
<div class="ui label basic small" v-for="(list, index) in lists">
|
||||
<a :href="'/servers/iplists/list?listId=' + list.id" title="点击查看详情" style="opacity: 1">{{list.name}}</a>
|
||||
<a href="" title="删除" @click.prevent="remove(index, list.id)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
241
EdgeAdmin/web/public/js/components/iplist/ip-list-table.js
Normal file
241
EdgeAdmin/web/public/js/components/iplist/ip-list-table.js
Normal file
@@ -0,0 +1,241 @@
|
||||
Vue.component("ip-list-table", {
|
||||
props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/],
|
||||
data: function () {
|
||||
let maxDeletes = 10000
|
||||
if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) {
|
||||
maxDeletes = this.vTotal
|
||||
}
|
||||
|
||||
return {
|
||||
items: this.vItems,
|
||||
keyword: (this.vKeyword != null) ? this.vKeyword : "",
|
||||
selectedAll: false,
|
||||
hasSelectedItems: false,
|
||||
|
||||
MaxDeletes: maxDeletes
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateItem: function (itemId) {
|
||||
this.$emit("update-item", itemId)
|
||||
},
|
||||
deleteItem: function (itemId) {
|
||||
this.$emit("delete-item", itemId)
|
||||
},
|
||||
viewLogs: function (itemId) {
|
||||
teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, {
|
||||
width: "50em",
|
||||
height: "30em"
|
||||
})
|
||||
},
|
||||
changeSelectedAll: function () {
|
||||
let boxes = this.$refs.itemCheckBox
|
||||
if (boxes == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
boxes.forEach(function (box) {
|
||||
box.checked = that.selectedAll
|
||||
})
|
||||
|
||||
this.hasSelectedItems = this.selectedAll
|
||||
},
|
||||
changeSelected: function (e) {
|
||||
let that = this
|
||||
that.hasSelectedItems = false
|
||||
let boxes = that.$refs.itemCheckBox
|
||||
if (boxes == null) {
|
||||
return
|
||||
}
|
||||
boxes.forEach(function (box) {
|
||||
if (box.checked) {
|
||||
that.hasSelectedItems = true
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteAll: function () {
|
||||
let boxes = this.$refs.itemCheckBox
|
||||
if (boxes == null) {
|
||||
return
|
||||
}
|
||||
let itemIds = []
|
||||
boxes.forEach(function (box) {
|
||||
if (box.checked) {
|
||||
itemIds.push(box.value)
|
||||
}
|
||||
})
|
||||
if (itemIds.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
Tea.action("/servers/iplists/deleteItems")
|
||||
.post()
|
||||
.params({
|
||||
itemIds: itemIds
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
|
||||
})
|
||||
},
|
||||
deleteCount: function () {
|
||||
let that = this
|
||||
teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗?", function () {
|
||||
let query = window.location.search
|
||||
if (query.startsWith("?")) {
|
||||
query = query.substring(1)
|
||||
}
|
||||
Tea.action("/servers/iplists/deleteCount?" + query)
|
||||
.post()
|
||||
.params({count: that.MaxDeletes})
|
||||
.success(function () {
|
||||
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
|
||||
})
|
||||
})
|
||||
},
|
||||
formatSeconds: function (seconds) {
|
||||
if (seconds < 60) {
|
||||
return seconds + "秒"
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return Math.ceil(seconds / 60) + "分钟"
|
||||
}
|
||||
if (seconds < 86400) {
|
||||
return Math.ceil(seconds / 3600) + "小时"
|
||||
}
|
||||
return Math.ceil(seconds / 86400) + "天"
|
||||
},
|
||||
cancelChecked: function () {
|
||||
this.hasSelectedItems = false
|
||||
this.selectedAll = false
|
||||
|
||||
let boxes = this.$refs.itemCheckBox
|
||||
if (boxes == null) {
|
||||
return
|
||||
}
|
||||
boxes.forEach(function (box) {
|
||||
box.checked = false
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-show="hasSelectedItems">
|
||||
<div class="ui divider"></div>
|
||||
<button class="ui button basic" type="button" @click.prevent="deleteAll">批量删除所选</button>
|
||||
|
||||
<button class="ui button basic" type="button" @click.prevent="deleteCount" v-if="vTotal != null && vTotal >= MaxDeletes">批量删除所有搜索结果({{MaxDeletes}}个)</button>
|
||||
|
||||
|
||||
<button class="ui button basic" type="button" @click.prevent="cancelChecked">取消选中</button>
|
||||
</div>
|
||||
<table class="ui table selectable celled" v-if="items.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1em">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="selectedAll" @change="changeSelectedAll"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width:18em">IP</th>
|
||||
<th style="width: 6em">类型</th>
|
||||
<th style="width: 6em">级别</th>
|
||||
<th style="width: 12em">过期时间</th>
|
||||
<th>备注</th>
|
||||
<th class="three op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="item in items">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" :value="item.id" @change="changeSelected" ref="itemCheckBox"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="item.type != 'all'" :class="{green: item.list != null && item.list.type == 'white', grey: item.list != null && item.list.type == 'grey'}">
|
||||
<span v-if="item.value != null && item.value.length > 0"><keyword :v-word="keyword">{{item.value}}</keyword></span>
|
||||
<span v-else>
|
||||
<keyword :v-word="keyword">{{item.ipFrom}}</keyword> <span> <span class="small red" v-if="item.isRead != null && !item.isRead"> New </span> <a :href="'/servers/iplists?ip=' + item.ipFrom" v-if="vShowSearchButton" title="搜索此IP"><span><i class="icon search small" style="color: #ccc"></i></span></a></span>
|
||||
<span v-if="item.ipTo.length > 0"> - <keyword :v-word="keyword">{{item.ipTo}}</keyword></span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="disabled">*</span>
|
||||
|
||||
<div v-if="item.region != null && item.region.length > 0">
|
||||
<span class="grey small">{{item.region}}</span>
|
||||
<span v-if="item.isp != null && item.isp.length > 0 && item.isp != '内网IP'" class="grey small"><span class="disabled">|</span> {{item.isp}}</span>
|
||||
</div>
|
||||
<div v-else-if="item.isp != null && item.isp.length > 0 && item.isp != '内网IP'"><span class="grey small">{{item.isp}}</span></div>
|
||||
|
||||
<div v-if="item.createdTime != null">
|
||||
<span class="small grey">添加于 {{item.createdTime}}
|
||||
<span v-if="item.list != null && item.list.id > 0">
|
||||
@
|
||||
|
||||
<a :href="'/servers/iplists/list?listId=' + item.list.id" v-if="item.policy.id == 0"><span>[<span v-if="item.list.type == 'black'">黑</span><span v-if="item.list.type == 'white'">白</span><span v-if="item.list.type == 'grey'">灰</span>名单:{{item.list.name}}]</span></a>
|
||||
<span v-else>[<span v-if="item.list.type == 'black'">黑</span><span v-if="item.list.type == 'white'">白</span><span v-if="item.list.type == 'grey'">灰</span>名单:{{item.list.name}}</span>
|
||||
|
||||
<span v-if="item.policy.id > 0">
|
||||
<span v-if="item.policy.server != null">
|
||||
<a :href="'/servers/server/settings/waf/ipadmin/allowList?serverId=' + item.policy.server.id + '&firewallPolicyId=' + item.policy.id" v-if="item.list.type == 'white'">[网站:{{item.policy.server.name}}]</a>
|
||||
<a :href="'/servers/server/settings/waf/ipadmin/denyList?serverId=' + item.policy.server.id + '&firewallPolicyId=' + item.policy.id" v-if="item.list.type == 'black'">[网站:{{item.policy.server.name}}]</a>
|
||||
<a :href="'/servers/server/settings/waf/ipadmin/greyList?serverId=' + item.policy.server.id + '&firewallPolicyId=' + item.policy.id" v-if="item.list.type == 'grey'">[网站:{{item.policy.server.name}}]</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
<a :href="'/servers/components/waf/ipadmin/lists?firewallPolicyId=' + item.policy.id + '&type=' + item.list.type">[策略:{{item.policy.name}}]</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="item.type.length == 0">IPv4</span>
|
||||
<span v-else-if="item.type == 'ipv4'">IPv4</span>
|
||||
<span v-else-if="item.type == 'ipv6'">IPv6</span>
|
||||
<span v-else-if="item.type == 'all'"><strong>所有IP</strong></span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="item.eventLevelName != null && item.eventLevelName.length > 0">{{item.eventLevelName}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="item.expiredTime.length > 0">
|
||||
{{item.expiredTime}}
|
||||
<div v-if="item.isExpired && item.lifeSeconds == null" style="margin-top: 0.5em">
|
||||
<span class="ui label tiny basic red">已过期</span>
|
||||
</div>
|
||||
<div v-if="item.lifeSeconds != null">
|
||||
<span class="small grey" v-if="item.lifeSeconds > 0">{{formatSeconds(item.lifeSeconds)}}</span>
|
||||
<span class="small red" v-if="item.lifeSeconds < 0">已过期</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="disabled">不过期</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="item.reason.length > 0">{{item.reason}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
|
||||
<div v-if="item.sourceNode != null && item.sourceNode.id > 0" style="margin-top: 0.4em">
|
||||
<a :href="'/clusters/cluster/node?clusterId=' + item.sourceNode.clusterId + '&nodeId=' + item.sourceNode.id"><span class="small"><i class="icon cloud"></i>{{item.sourceNode.name}}</span></a>
|
||||
</div>
|
||||
<div style="margin-top: 0.4em" v-if="item.sourceServer != null && item.sourceServer.id > 0">
|
||||
<a :href="'/servers/server?serverId=' + item.sourceServer.id" style="border: 0"><span class="small "><i class="icon clone outline"></i>{{item.sourceServer.name}}</span></a>
|
||||
</div>
|
||||
<div v-if="item.sourcePolicy != null && item.sourcePolicy.id > 0" style="margin-top: 0.4em">
|
||||
<a :href="'/servers/components/waf/group?firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id + '#set' + item.sourceSet.id" v-if="item.sourcePolicy.serverId == 0"><span class="small "><i class="icon shield"></i>{{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
||||
<a :href="'/servers/server/settings/waf/group?serverId=' + item.sourcePolicy.serverId + '&firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id + '#set' + item.sourceSet.id" v-if="item.sourcePolicy.serverId > 0"><span class="small "><i class="icon shield"></i> {{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="viewLogs(item.id)">日志</a>
|
||||
<a href="" @click.prevent="updateItem(item.id)">修改</a>
|
||||
<a href="" @click.prevent="deleteItem(item.id)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
268
EdgeAdmin/web/public/js/components/maps/traffic-map-box.js
Normal file
268
EdgeAdmin/web/public/js/components/maps/traffic-map-box.js
Normal file
@@ -0,0 +1,268 @@
|
||||
Vue.component("traffic-map-box", {
|
||||
props: ["v-stats", "v-is-attack"],
|
||||
mounted: function () {
|
||||
this.render()
|
||||
},
|
||||
data: function () {
|
||||
let maxPercent = 0
|
||||
let isAttack = this.vIsAttack
|
||||
this.vStats.forEach(function (v) {
|
||||
let percent = parseFloat(v.percent)
|
||||
if (percent > maxPercent) {
|
||||
maxPercent = percent
|
||||
}
|
||||
|
||||
v.formattedCountRequests = teaweb.formatCount(v.countRequests) + "次"
|
||||
v.formattedCountAttackRequests = teaweb.formatCount(v.countAttackRequests) + "次"
|
||||
})
|
||||
|
||||
if (maxPercent < 100) {
|
||||
maxPercent *= 1.2 // 不要让某一项100%
|
||||
}
|
||||
|
||||
let screenIsNarrow = window.innerWidth < 512
|
||||
|
||||
return {
|
||||
isAttack: isAttack,
|
||||
stats: this.vStats,
|
||||
chart: null,
|
||||
minOpacity: 0.2,
|
||||
maxPercent: maxPercent,
|
||||
selectedCountryName: "",
|
||||
screenIsNarrow: screenIsNarrow
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
render: function () {
|
||||
if (this.$el.offsetWidth < 300) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.render()
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
this.chart = teaweb.initChart(document.getElementById("traffic-map-box"));
|
||||
let that = this
|
||||
this.chart.setOption({
|
||||
backgroundColor: "white",
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0
|
||||
},
|
||||
roam: false,
|
||||
tooltip: {
|
||||
trigger: "item"
|
||||
},
|
||||
series: [{
|
||||
type: "map",
|
||||
map: "world",
|
||||
zoom: 1.3,
|
||||
selectedMode: false,
|
||||
itemStyle: {
|
||||
areaColor: "#E9F0F9",
|
||||
borderColor: "#DDD"
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: "10px",
|
||||
color: "#fff",
|
||||
backgroundColor: "#8B9BD3",
|
||||
padding: [2, 2, 2, 2]
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: "#8B9BD3",
|
||||
opacity: 1.0
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: "10px",
|
||||
color: "#fff",
|
||||
backgroundColor: "#8B9BD3",
|
||||
padding: [2, 2, 2, 2]
|
||||
}
|
||||
},
|
||||
//select: {itemStyle:{ areaColor: "#8B9BD3", opacity: 0.8 }},
|
||||
tooltip: {
|
||||
formatter: function (args) {
|
||||
let name = args.name
|
||||
let stat = null
|
||||
that.stats.forEach(function (v) {
|
||||
if (v.name == name) {
|
||||
stat = v
|
||||
}
|
||||
})
|
||||
|
||||
if (stat != null) {
|
||||
return name + "<br/>流量:" + stat.formattedBytes + "<br/>流量占比:" + stat.percent + "%<br/>请求数:" + stat.formattedCountRequests + "<br/>攻击数:" + stat.formattedCountAttackRequests
|
||||
}
|
||||
return name
|
||||
}
|
||||
},
|
||||
data: this.stats.map(function (v) {
|
||||
let opacity = parseFloat(v.percent) / that.maxPercent
|
||||
if (opacity < that.minOpacity) {
|
||||
opacity = that.minOpacity
|
||||
}
|
||||
let fullOpacity = opacity * 3
|
||||
if (fullOpacity > 1) {
|
||||
fullOpacity = 1
|
||||
}
|
||||
let isAttack = that.vIsAttack
|
||||
let bgColor = "#276AC6"
|
||||
if (isAttack) {
|
||||
bgColor = "#B03A5B"
|
||||
}
|
||||
|
||||
return {
|
||||
name: v.name,
|
||||
value: v.bytes,
|
||||
percent: parseFloat(v.percent),
|
||||
itemStyle: {
|
||||
areaColor: bgColor,
|
||||
opacity: opacity
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: bgColor,
|
||||
opacity: fullOpacity
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (args) {
|
||||
return args.name
|
||||
}
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: function (args) {
|
||||
if (args.name == that.selectedCountryName) {
|
||||
return args.name
|
||||
}
|
||||
return ""
|
||||
},
|
||||
fontSize: "10px",
|
||||
color: "#fff",
|
||||
backgroundColor: "#8B9BD3",
|
||||
padding: [2, 2, 2, 2]
|
||||
}
|
||||
}
|
||||
}),
|
||||
nameMap: window.WorldCountriesMap
|
||||
}]
|
||||
})
|
||||
this.chart.resize()
|
||||
},
|
||||
selectCountry: function (countryName) {
|
||||
if (this.chart == null) {
|
||||
return
|
||||
}
|
||||
let option = this.chart.getOption()
|
||||
let that = this
|
||||
option.series[0].data.forEach(function (v) {
|
||||
let opacity = v.percent / that.maxPercent
|
||||
if (opacity < that.minOpacity) {
|
||||
opacity = that.minOpacity
|
||||
}
|
||||
|
||||
if (v.name == countryName) {
|
||||
if (v.isSelected) {
|
||||
v.itemStyle.opacity = opacity
|
||||
v.isSelected = false
|
||||
v.label.show = false
|
||||
that.selectedCountryName = ""
|
||||
return
|
||||
}
|
||||
v.isSelected = true
|
||||
that.selectedCountryName = countryName
|
||||
opacity *= 3
|
||||
if (opacity > 1) {
|
||||
opacity = 1
|
||||
}
|
||||
|
||||
// 至少是0.5,让用户能够看清
|
||||
if (opacity < 0.5) {
|
||||
opacity = 0.5
|
||||
}
|
||||
v.itemStyle.opacity = opacity
|
||||
v.label.show = true
|
||||
} else {
|
||||
v.itemStyle.opacity = opacity
|
||||
v.isSelected = false
|
||||
v.label.show = false
|
||||
}
|
||||
})
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
select: function (args) {
|
||||
this.selectCountry(args.countryName)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table style="width: 100%; border: 0; padding: 0; margin: 0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="traffic-map-box" id="traffic-map-box"></div>
|
||||
</td>
|
||||
<td style="width: 14em" v-if="!screenIsNarrow">
|
||||
<traffic-map-box-table :v-stats="stats" :v-is-attack="isAttack" @select="select"></traffic-map-box-table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-if="screenIsNarrow">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<traffic-map-box-table :v-stats="stats" :v-is-attack="isAttack" :v-screen-is-narrow="true" @select="select"></traffic-map-box-table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("traffic-map-box-table", {
|
||||
props: ["v-stats", "v-is-attack", "v-screen-is-narrow"],
|
||||
data: function () {
|
||||
return {
|
||||
stats: this.vStats,
|
||||
isAttack: this.vIsAttack
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select: function (countryName) {
|
||||
this.$emit("select", {countryName: countryName})
|
||||
}
|
||||
},
|
||||
template: `<div style="overflow-y: auto" :style="{'max-height':vScreenIsNarrow ? 'auto' : '16em'}" class="narrow-scrollbar">
|
||||
<table class="ui table selectable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">国家/地区排行 <tip-icon content="只有开启了统计的网站才会有记录。"></tip-icon></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="stats.length == 0">
|
||||
<tr>
|
||||
<td colspan="2">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-for="(stat, index) in stats.slice(0, 10)">
|
||||
<td @click.prevent="select(stat.name)" style="cursor: pointer" colspan="2">
|
||||
<div class="ui progress bar" :class="{red: vIsAttack, blue:!vIsAttack}" style="margin-bottom: 0.3em">
|
||||
<div class="bar" style="min-width: 0; height: 4px;" :style="{width: stat.percent + '%'}"></div>
|
||||
</div>
|
||||
<div>{{stat.name}}</div>
|
||||
<div><span class="grey">{{stat.percent}}% </span>
|
||||
<span class="small grey" v-if="isAttack">{{stat.formattedCountAttackRequests}}</span>
|
||||
<span class="small grey" v-if="!isAttack">({{stat.formattedBytes}})</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
Vue.component("message-media-instance-selector", {
|
||||
props: ["v-instance-id"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/admins/recipients/instances/options")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.instances = resp.data.instances
|
||||
|
||||
// 初始化简介
|
||||
if (that.instanceId > 0) {
|
||||
let instance = that.instances.$find(function (_, instance) {
|
||||
return instance.id == that.instanceId
|
||||
})
|
||||
if (instance != null) {
|
||||
that.description = instance.description
|
||||
that.update(instance.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let instanceId = this.vInstanceId
|
||||
if (instanceId == null) {
|
||||
instanceId = 0
|
||||
}
|
||||
return {
|
||||
instances: [],
|
||||
description: "",
|
||||
instanceId: instanceId
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
instanceId: function (v) {
|
||||
this.update(v)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function (v) {
|
||||
let instance = this.instances.$find(function (_, instance) {
|
||||
return instance.id == v
|
||||
})
|
||||
if (instance == null) {
|
||||
this.description = ""
|
||||
} else {
|
||||
this.description = instance.description
|
||||
}
|
||||
this.$emit("change", instance)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<select class="ui dropdown auto-width" name="instanceId" v-model="instanceId">
|
||||
<option value="0">[选择媒介]</option>
|
||||
<option v-for="instance in instances" :value="instance.id">{{instance.name}} ({{instance.media.name}})</option>
|
||||
</select>
|
||||
<p class="comment" v-html="description"></p>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
Vue.component("message-media-selector", {
|
||||
props: ["v-media-type"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/admins/recipients/mediaOptions")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.medias = resp.data.medias
|
||||
|
||||
// 初始化简介
|
||||
if (that.mediaType.length > 0) {
|
||||
let media = that.medias.$find(function (_, media) {
|
||||
return media.type == that.mediaType
|
||||
})
|
||||
if (media != null) {
|
||||
that.description = media.description
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let mediaType = this.vMediaType
|
||||
if (mediaType == null) {
|
||||
mediaType = ""
|
||||
}
|
||||
return {
|
||||
medias: [],
|
||||
description: "",
|
||||
mediaType: mediaType
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
mediaType: function (v) {
|
||||
let media = this.medias.$find(function (_, media) {
|
||||
return media.type == v
|
||||
})
|
||||
if (media == null) {
|
||||
this.description = ""
|
||||
} else {
|
||||
this.description = media.description
|
||||
}
|
||||
this.$emit("change", media)
|
||||
},
|
||||
},
|
||||
template: `<div>
|
||||
<select class="ui dropdown auto-width" name="mediaType" v-model="mediaType">
|
||||
<option value="">[选择媒介类型]</option>
|
||||
<option v-for="media in medias" :value="media.type">{{media.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-html="description"></p>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
// 消息接收人设置
|
||||
Vue.component("message-receivers-box", {
|
||||
props: ["v-node-cluster-id"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/clusters/cluster/settings/message/selectedReceivers")
|
||||
.params({
|
||||
clusterId: this.clusterId
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.receivers = resp.data.receivers
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let clusterId = this.vNodeClusterId
|
||||
if (clusterId == null) {
|
||||
clusterId = 0
|
||||
}
|
||||
return {
|
||||
clusterId: clusterId,
|
||||
receivers: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addReceiver: function () {
|
||||
let that = this
|
||||
let recipientIdStrings = []
|
||||
let groupIdStrings = []
|
||||
this.receivers.forEach(function (v) {
|
||||
if (v.type == "recipient") {
|
||||
recipientIdStrings.push(v.id.toString())
|
||||
} else if (v.type == "group") {
|
||||
groupIdStrings.push(v.id.toString())
|
||||
}
|
||||
})
|
||||
|
||||
teaweb.popup("/clusters/cluster/settings/message/selectReceiverPopup?recipientIds=" + recipientIdStrings.join(",") + "&groupIds=" + groupIdStrings.join(","), {
|
||||
callback: function (resp) {
|
||||
that.receivers.push(resp.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeReceiver: function (index) {
|
||||
this.receivers.$remove(index)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="receiversJSON" :value="JSON.stringify(receivers)"/>
|
||||
<div v-if="receivers.length > 0">
|
||||
<div v-for="(receiver, index) in receivers" class="ui label basic small">
|
||||
<span v-if="receiver.type == 'group'">分组:</span>{{receiver.name}} <span class="grey small" v-if="receiver.subName != null && receiver.subName.length > 0">({{receiver.subName}})</span> <a href="" title="删除" @click.prevent="removeReceiver(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button type="button" class="ui button tiny" @click.prevent="addReceiver">+</button>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
Vue.component("message-recipient-group-selector", {
|
||||
props: ["v-groups"],
|
||||
data: function () {
|
||||
let groups = this.vGroups
|
||||
if (groups == null) {
|
||||
groups = []
|
||||
}
|
||||
let groupIds = []
|
||||
if (groups.length > 0) {
|
||||
groupIds = groups.map(function (v) {
|
||||
return v.id.toString()
|
||||
}).join(",")
|
||||
}
|
||||
|
||||
return {
|
||||
groups: groups,
|
||||
groupIds: groupIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addGroup: function () {
|
||||
let that = this
|
||||
teaweb.popup("/admins/recipients/groups/selectPopup?groupIds=" + this.groupIds, {
|
||||
callback: function (resp) {
|
||||
that.groups.push(resp.data.group)
|
||||
that.update()
|
||||
}
|
||||
})
|
||||
},
|
||||
removeGroup: function (index) {
|
||||
this.groups.$remove(index)
|
||||
this.update()
|
||||
},
|
||||
update: function () {
|
||||
let groupIds = []
|
||||
if (this.groups.length > 0) {
|
||||
this.groups.forEach(function (v) {
|
||||
groupIds.push(v.id)
|
||||
})
|
||||
}
|
||||
this.groupIds = groupIds.join(",")
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="groupIds" :value="groupIds"/>
|
||||
<div v-if="groups.length > 0">
|
||||
<div>
|
||||
<div v-for="(group, index) in groups" class="ui label small basic">
|
||||
{{group.name}} <a href="" title="删除" @click.prevent="removeGroup(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="addGroup()">+</button>
|
||||
</div>`
|
||||
})
|
||||
113
EdgeAdmin/web/public/js/components/messages/message-row.js
Normal file
113
EdgeAdmin/web/public/js/components/messages/message-row.js
Normal file
@@ -0,0 +1,113 @@
|
||||
Vue.component("message-row", {
|
||||
props: ["v-message", "v-can-close"],
|
||||
data: function () {
|
||||
let paramsJSON = this.vMessage.params
|
||||
let params = null
|
||||
if (paramsJSON != null && paramsJSON.length > 0) {
|
||||
params = JSON.parse(paramsJSON)
|
||||
}
|
||||
|
||||
return {
|
||||
message: this.vMessage,
|
||||
params: params,
|
||||
isClosing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
viewCert: function (certId) {
|
||||
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
|
||||
height: "28em",
|
||||
width: "48em"
|
||||
})
|
||||
},
|
||||
readMessage: function (messageId) {
|
||||
let that = this
|
||||
|
||||
Tea.action("/messages/readPage")
|
||||
.params({"messageIds": [messageId]})
|
||||
.post()
|
||||
.success(function () {
|
||||
// 刷新父级页面Badge
|
||||
if (window.parent.Tea != null && window.parent.Tea.Vue != null) {
|
||||
window.parent.Tea.Vue.checkMessagesOnce()
|
||||
}
|
||||
|
||||
// 刷新当前页面
|
||||
if (that.vCanClose && typeof (NotifyPopup) != "undefined") {
|
||||
that.isClosing = true
|
||||
setTimeout(function () {
|
||||
NotifyPopup({})
|
||||
}, 1000)
|
||||
} else {
|
||||
teaweb.reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table class="ui table selectable" v-if="!isClosing">
|
||||
<tr :class="{error: message.level == 'error', positive: message.level == 'success', warning: message.level == 'warning'}">
|
||||
<td style="position: relative">
|
||||
<strong>{{message.datetime}}</strong>
|
||||
<span v-if="message.cluster != null && message.cluster.id != null">
|
||||
<span> | </span>
|
||||
<a :href="'/clusters/cluster?clusterId=' + message.cluster.id" target="_top" v-if="message.role == 'node'">集群:{{message.cluster.name}}</a>
|
||||
<a :href="'/ns/clusters/cluster?clusterId=' + message.cluster.id" target="_top" v-if="message.role == 'dns'">DNS集群:{{message.cluster.name}}</a>
|
||||
</span>
|
||||
<span v-if="message.node != null && message.node.id != null">
|
||||
<span> | </span>
|
||||
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id" target="_top" v-if="message.role == 'node'">节点:{{message.node.name}}</a>
|
||||
<a :href="'/ns/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id" target="_top" v-if="message.role == 'dns'">DNS节点:{{message.node.name}}</a>
|
||||
</span>
|
||||
<a href="" style="position: absolute; right: 1em" @click.prevent="readMessage(message.id)" title="标为已读"><i class="icon check"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr :class="{error: message.level == 'error', positive: message.level == 'success', warning: message.level == 'warning'}">
|
||||
<td>
|
||||
<pre style="padding: 0; margin:0; word-break: break-all;">{{message.body}}</pre>
|
||||
|
||||
<!-- 健康检查 -->
|
||||
<div v-if="message.type == 'HealthCheckFailed'" style="margin-top: 0.8em">
|
||||
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + param.node.id" v-for="param in params" class="ui label small basic" style="margin-bottom: 0.5em" target="_top">{{param.node.name}}: {{param.error}}</a>
|
||||
</div>
|
||||
|
||||
<!-- 集群DNS设置 -->
|
||||
<div v-if="message.type == 'ClusterDNSSyncFailed'" style="margin-top: 0.8em">
|
||||
<a :href="'/dns/clusters/cluster?clusterId=' + message.cluster.id" target="_top">查看问题 »</a>
|
||||
</div>
|
||||
|
||||
<!-- 证书即将过期 -->
|
||||
<div v-if="message.type == 'SSLCertExpiring'" style="margin-top: 0.8em">
|
||||
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a><span v-if="params != null && params.acmeTaskId > 0"> | <a :href="'/servers/certs/acme'" target="_top">查看任务»</a></span>
|
||||
</div>
|
||||
|
||||
<!-- 证书续期成功 -->
|
||||
<div v-if="message.type == 'SSLCertACMETaskSuccess'" style="margin-top: 0.8em">
|
||||
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> | <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务»</a>
|
||||
</div>
|
||||
|
||||
<!-- 证书续期失败 -->
|
||||
<div v-if="message.type == 'SSLCertACMETaskFailed'" style="margin-top: 0.8em">
|
||||
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> | <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务»</a>
|
||||
</div>
|
||||
|
||||
<!-- 网站域名审核 -->
|
||||
<div v-if="message.type == 'serverNamesRequireAuditing'" style="margin-top: 0.8em">
|
||||
<a :href="'/servers/server/settings/serverNames?serverId=' + params.serverId" target="_top">去审核</a></a>
|
||||
</div>
|
||||
|
||||
<!-- 节点调度 -->
|
||||
<div v-if="message.type == 'NodeSchedule'" style="margin-top: 0.8em">
|
||||
<a :href="'/clusters/cluster/node/settings/schedule?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id" target="_top">查看调度状态 »</a>
|
||||
</div>
|
||||
|
||||
<!-- 节点租期结束 -->
|
||||
<div v-if="message.type == 'NodeOfflineDay'" style="margin-top: 0.8em">
|
||||
<a :href="'/clusters/cluster/node/detail?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id" target="_top">查看详情 »</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
Vue.component("node-cache-disk-dirs-box", {
|
||||
props: ["value", "name"],
|
||||
data: function () {
|
||||
let dirs = this.value
|
||||
if (dirs == null) {
|
||||
dirs = []
|
||||
}
|
||||
return {
|
||||
dirs: dirs,
|
||||
|
||||
isEditing: false,
|
||||
isAdding: false,
|
||||
|
||||
addingPath: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingPath.focus()
|
||||
}, 100)
|
||||
},
|
||||
confirm: function () {
|
||||
let addingPath = this.addingPath.trim()
|
||||
if (addingPath.length == 0) {
|
||||
let that = this
|
||||
teaweb.warn("请输入要添加的缓存目录", function () {
|
||||
that.$refs.addingPath.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
if (addingPath[0] != "/") {
|
||||
addingPath = "/" + addingPath
|
||||
}
|
||||
this.dirs.push({
|
||||
path: addingPath
|
||||
})
|
||||
this.cancel()
|
||||
},
|
||||
cancel: function () {
|
||||
this.addingPath = ""
|
||||
this.isAdding = false
|
||||
this.isEditing = false
|
||||
},
|
||||
remove: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此目录吗?", function () {
|
||||
that.dirs.$remove(index)
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="name" :value="JSON.stringify(dirs)"/>
|
||||
<div style="margin-bottom: 0.3em">
|
||||
<span class="ui label small basic" v-for="(dir, index) in dirs">
|
||||
<i class="icon folder"></i>{{dir.path}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 添加 -->
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" style="width: 30em" v-model="addingPath" @keyup.enter="confirm()" @keypress.enter.prevent="1" @keydown.esc="cancel()" ref="addingPath" placeholder="新的缓存目录,比如 /mnt/cache"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button small" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
20
EdgeAdmin/web/public/js/components/node/node-combo-box.js
Normal file
20
EdgeAdmin/web/public/js/components/node/node-combo-box.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Vue.component("node-combo-box", {
|
||||
props: ["v-cluster-id", "v-node-id"],
|
||||
data: function () {
|
||||
let that = this
|
||||
Tea.action("/clusters/nodeOptions")
|
||||
.params({
|
||||
clusterId: this.vClusterId
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.nodes = resp.data.nodes
|
||||
})
|
||||
return {
|
||||
nodes: []
|
||||
}
|
||||
},
|
||||
template: `<div v-if="nodes.length > 0">
|
||||
<combo-box title="节点" placeholder="节点名称" :v-items="nodes" name="nodeId" :v-value="vNodeId"></combo-box>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
Vue.component("node-group-selector", {
|
||||
props: ["v-cluster-id", "v-group"],
|
||||
data: function () {
|
||||
return {
|
||||
selectedGroup: this.vGroup
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectGroup: function () {
|
||||
let that = this
|
||||
teaweb.popup("/clusters/cluster/groups/selectPopup?clusterId=" + this.vClusterId, {
|
||||
callback: function (resp) {
|
||||
that.selectedGroup = resp.data.group
|
||||
}
|
||||
})
|
||||
},
|
||||
addGroup: function () {
|
||||
let that = this
|
||||
teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.vClusterId, {
|
||||
callback: function (resp) {
|
||||
that.selectedGroup = resp.data.group
|
||||
}
|
||||
})
|
||||
},
|
||||
removeGroup: function () {
|
||||
this.selectedGroup = null
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui label small basic" v-if="selectedGroup != null">
|
||||
<input type="hidden" name="groupId" :value="selectedGroup.id"/>
|
||||
{{selectedGroup.name}} <a href="" title="删除" @click.prevent="removeGroup()"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div v-if="selectedGroup == null">
|
||||
<a href="" @click.prevent="selectGroup()">[选择分组]</a> <a href="" @click.prevent="addGroup()">[添加分组]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
Vue.component("node-ip-address-clusters-selector", {
|
||||
props: ["vClusters"],
|
||||
mounted: function () {
|
||||
this.checkClusters()
|
||||
},
|
||||
data: function () {
|
||||
let clusters = this.vClusters
|
||||
if (clusters == null) {
|
||||
clusters = []
|
||||
}
|
||||
return {
|
||||
clusters: clusters,
|
||||
hasCheckedCluster: false,
|
||||
clustersVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkClusters: function () {
|
||||
let that = this
|
||||
|
||||
let b = false
|
||||
this.clusters.forEach(function (cluster) {
|
||||
if (cluster.isChecked) {
|
||||
b = true
|
||||
}
|
||||
})
|
||||
|
||||
this.hasCheckedCluster = b
|
||||
|
||||
return b
|
||||
},
|
||||
changeCluster: function (cluster) {
|
||||
cluster.isChecked = !cluster.isChecked
|
||||
this.checkClusters()
|
||||
},
|
||||
showClusters: function () {
|
||||
this.clustersVisible = !this.clustersVisible
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="!hasCheckedCluster">默认用于所有集群 <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a></span>
|
||||
<div v-if="hasCheckedCluster">
|
||||
<span v-for="cluster in clusters" class="ui label basic small" v-if="cluster.isChecked">{{cluster.name}}</span> <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a>
|
||||
<p class="comment">当前IP仅在所选集群中有效。</p>
|
||||
</div>
|
||||
<div v-show="clustersVisible">
|
||||
<div class="ui divider"></div>
|
||||
<checkbox v-for="cluster in clusters" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" name="clusterIds">
|
||||
{{cluster.name}}
|
||||
</checkbox>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,563 @@
|
||||
// 节点IP阈值
|
||||
Vue.component("node-ip-address-thresholds-box", {
|
||||
props: ["v-thresholds"],
|
||||
data: function () {
|
||||
let thresholds = this.vThresholds
|
||||
if (thresholds == null) {
|
||||
thresholds = []
|
||||
} else {
|
||||
thresholds.forEach(function (v) {
|
||||
if (v.items == null) {
|
||||
v.items = []
|
||||
}
|
||||
if (v.actions == null) {
|
||||
v.actions = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
editingIndex: -1,
|
||||
thresholds: thresholds,
|
||||
addingThreshold: {
|
||||
items: [],
|
||||
actions: []
|
||||
},
|
||||
isAdding: false,
|
||||
isAddingItem: false,
|
||||
isAddingAction: false,
|
||||
|
||||
itemCode: "nodeAvgRequests",
|
||||
itemReportGroups: [],
|
||||
itemOperator: "lte",
|
||||
itemValue: "",
|
||||
itemDuration: "5",
|
||||
allItems: window.IP_ADDR_THRESHOLD_ITEMS,
|
||||
allOperators: [
|
||||
{
|
||||
"name": "小于等于",
|
||||
"code": "lte"
|
||||
},
|
||||
{
|
||||
"name": "大于",
|
||||
"code": "gt"
|
||||
},
|
||||
{
|
||||
"name": "不等于",
|
||||
"code": "neq"
|
||||
},
|
||||
{
|
||||
"name": "小于",
|
||||
"code": "lt"
|
||||
},
|
||||
{
|
||||
"name": "大于等于",
|
||||
"code": "gte"
|
||||
}
|
||||
],
|
||||
allActions: window.IP_ADDR_THRESHOLD_ACTIONS,
|
||||
|
||||
actionCode: "up",
|
||||
actionBackupIPs: "",
|
||||
actionWebHookURL: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = !this.isAdding
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.editingIndex = -1
|
||||
this.addingThreshold = {
|
||||
items: [],
|
||||
actions: []
|
||||
}
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.addingThreshold.items.length == 0) {
|
||||
teaweb.warn("需要至少添加一个阈值")
|
||||
return
|
||||
}
|
||||
if (this.addingThreshold.actions.length == 0) {
|
||||
teaweb.warn("需要至少添加一个动作")
|
||||
return
|
||||
}
|
||||
|
||||
if (this.editingIndex >= 0) {
|
||||
this.thresholds[this.editingIndex].items = this.addingThreshold.items
|
||||
this.thresholds[this.editingIndex].actions = this.addingThreshold.actions
|
||||
} else {
|
||||
this.thresholds.push({
|
||||
items: this.addingThreshold.items,
|
||||
actions: this.addingThreshold.actions
|
||||
})
|
||||
}
|
||||
|
||||
// 还原
|
||||
this.cancel()
|
||||
},
|
||||
remove: function (index) {
|
||||
this.cancel()
|
||||
this.thresholds.$remove(index)
|
||||
},
|
||||
update: function (index) {
|
||||
this.editingIndex = index
|
||||
this.addingThreshold = {
|
||||
items: this.thresholds[index].items.$copy(),
|
||||
actions: this.thresholds[index].actions.$copy()
|
||||
}
|
||||
this.isAdding = true
|
||||
},
|
||||
|
||||
addItem: function () {
|
||||
this.isAddingItem = !this.isAddingItem
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.itemValue.focus()
|
||||
}, 100)
|
||||
},
|
||||
cancelItem: function () {
|
||||
this.isAddingItem = false
|
||||
|
||||
this.itemCode = "nodeAvgRequests"
|
||||
this.itmeOperator = "lte"
|
||||
this.itemValue = ""
|
||||
this.itemDuration = "5"
|
||||
this.itemReportGroups = []
|
||||
},
|
||||
confirmItem: function () {
|
||||
// 特殊阈值快速添加
|
||||
if (["nodeHealthCheck"].$contains(this.itemCode)) {
|
||||
if (this.itemValue.toString().length == 0) {
|
||||
teaweb.warn("请选择检查结果")
|
||||
return
|
||||
}
|
||||
|
||||
let value = parseInt(this.itemValue)
|
||||
if (isNaN(value)) {
|
||||
value = 0
|
||||
} else if (value < 0) {
|
||||
value = 0
|
||||
} else if (value > 1) {
|
||||
value = 1
|
||||
}
|
||||
|
||||
// 添加
|
||||
this.addingThreshold.items.push({
|
||||
item: this.itemCode,
|
||||
operator: this.itemOperator,
|
||||
value: value,
|
||||
duration: 0,
|
||||
durationUnit: "minute",
|
||||
options: {}
|
||||
})
|
||||
this.cancelItem()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.itemDuration.length == 0) {
|
||||
let that = this
|
||||
teaweb.warn("请输入统计周期", function () {
|
||||
that.$refs.itemDuration.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
let itemDuration = parseInt(this.itemDuration)
|
||||
if (isNaN(itemDuration) || itemDuration <= 0) {
|
||||
teaweb.warn("请输入正确的统计周期", function () {
|
||||
that.$refs.itemDuration.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.itemValue.length == 0) {
|
||||
let that = this
|
||||
teaweb.warn("请输入对比值", function () {
|
||||
that.$refs.itemValue.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
let itemValue = parseFloat(this.itemValue)
|
||||
if (isNaN(itemValue)) {
|
||||
teaweb.warn("请输入正确的对比值", function () {
|
||||
that.$refs.itemValue.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let options = {}
|
||||
|
||||
switch (this.itemCode) {
|
||||
case "connectivity": // 连通性校验
|
||||
if (itemValue > 100) {
|
||||
let that = this
|
||||
teaweb.warn("连通性对比值不能超过100", function () {
|
||||
that.$refs.itemValue.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
options["groups"] = this.itemReportGroups
|
||||
break
|
||||
}
|
||||
|
||||
// 添加
|
||||
this.addingThreshold.items.push({
|
||||
item: this.itemCode,
|
||||
operator: this.itemOperator,
|
||||
value: itemValue,
|
||||
duration: itemDuration,
|
||||
durationUnit: "minute",
|
||||
options: options
|
||||
})
|
||||
|
||||
// 还原
|
||||
this.cancelItem()
|
||||
},
|
||||
removeItem: function (index) {
|
||||
this.cancelItem()
|
||||
this.addingThreshold.items.$remove(index)
|
||||
},
|
||||
changeReportGroups: function (groups) {
|
||||
this.itemReportGroups = groups
|
||||
},
|
||||
itemName: function (item) {
|
||||
let result = ""
|
||||
this.allItems.forEach(function (v) {
|
||||
if (v.code == item) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
itemUnitName: function (itemCode) {
|
||||
let result = ""
|
||||
this.allItems.forEach(function (v) {
|
||||
if (v.code == itemCode) {
|
||||
result = v.unit
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
itemDurationUnitName: function (unit) {
|
||||
switch (unit) {
|
||||
case "minute":
|
||||
return "分钟"
|
||||
case "second":
|
||||
return "秒"
|
||||
case "hour":
|
||||
return "小时"
|
||||
case "day":
|
||||
return "天"
|
||||
}
|
||||
return unit
|
||||
},
|
||||
itemOperatorName: function (operator) {
|
||||
let result = ""
|
||||
this.allOperators.forEach(function (v) {
|
||||
if (v.code == operator) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
addAction: function () {
|
||||
this.isAddingAction = !this.isAddingAction
|
||||
},
|
||||
cancelAction: function () {
|
||||
this.isAddingAction = false
|
||||
this.actionCode = "up"
|
||||
this.actionBackupIPs = ""
|
||||
this.actionWebHookURL = ""
|
||||
},
|
||||
confirmAction: function () {
|
||||
this.doConfirmAction(false)
|
||||
},
|
||||
doConfirmAction: function (validated, options) {
|
||||
// 是否已存在
|
||||
let exists = false
|
||||
let that = this
|
||||
this.addingThreshold.actions.forEach(function (v) {
|
||||
if (v.action == that.actionCode) {
|
||||
exists = true
|
||||
}
|
||||
})
|
||||
if (exists) {
|
||||
teaweb.warn("此动作已经添加过了,无需重复添加")
|
||||
return
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = {}
|
||||
}
|
||||
|
||||
switch (this.actionCode) {
|
||||
case "switch":
|
||||
if (!validated) {
|
||||
Tea.action("/ui/validateIPs")
|
||||
.params({
|
||||
"ips": this.actionBackupIPs
|
||||
})
|
||||
.success(function (resp) {
|
||||
if (resp.data.ips.length == 0) {
|
||||
teaweb.warn("请输入备用IP", function () {
|
||||
that.$refs.actionBackupIPs.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
options["ips"] = resp.data.ips
|
||||
that.doConfirmAction(true, options)
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("输入的IP '" + resp.data.failIP + "' 格式不正确,请改正后提交", function () {
|
||||
that.$refs.actionBackupIPs.focus()
|
||||
})
|
||||
})
|
||||
.post()
|
||||
return
|
||||
}
|
||||
break
|
||||
case "webHook":
|
||||
if (this.actionWebHookURL.length == 0) {
|
||||
teaweb.warn("请输入WebHook URL", function () {
|
||||
that.$refs.webHookURL.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this.actionWebHookURL.match(/^(http|https):\/\//i)) {
|
||||
teaweb.warn("URL开头必须是http://或者https://", function () {
|
||||
that.$refs.webHookURL.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
options["url"] = this.actionWebHookURL
|
||||
}
|
||||
|
||||
this.addingThreshold.actions.push({
|
||||
action: this.actionCode,
|
||||
options: options
|
||||
})
|
||||
|
||||
// 还原
|
||||
this.cancelAction()
|
||||
},
|
||||
removeAction: function (index) {
|
||||
this.cancelAction()
|
||||
this.addingThreshold.actions.$remove(index)
|
||||
},
|
||||
actionName: function (actionCode) {
|
||||
let result = ""
|
||||
this.allActions.forEach(function (v) {
|
||||
if (v.code == actionCode) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="thresholdsJSON" :value="JSON.stringify(thresholds)"/>
|
||||
|
||||
<!-- 已有条件 -->
|
||||
<div v-if="thresholds.length > 0">
|
||||
<div class="ui label basic small" v-for="(threshold, index) in thresholds">
|
||||
<span v-for="(item, itemIndex) in threshold.items">
|
||||
<span v-if="item.item != 'nodeHealthCheck'">
|
||||
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
|
||||
</span>
|
||||
{{itemName(item.item)}}
|
||||
|
||||
<span v-if="item.item == 'nodeHealthCheck'">
|
||||
<!-- 健康检查 -->
|
||||
<span v-if="item.value == 1">成功</span>
|
||||
<span v-if="item.value == 0">失败</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- 连通性 -->
|
||||
<span v-if="item.item == 'connectivity' && item.options != null && item.options.groups != null && item.options.groups.length > 0">[<span v-for="(group, groupIndex) in item.options.groups">{{group.name}} <span v-if="groupIndex != item.options.groups.length - 1"> </span></span>]</span>
|
||||
|
||||
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> {{item.value}}{{itemUnitName(item.item)}}
|
||||
</span>
|
||||
<span v-if="itemIndex != threshold.items.length - 1" style="font-style: italic">AND </span>
|
||||
</span>
|
||||
->
|
||||
<span v-for="(action, actionIndex) in threshold.actions">{{actionName(action.action)}}
|
||||
<span v-if="action.action == 'switch'">到{{action.options.ips.join(", ")}}</span>
|
||||
<span v-if="action.action == 'webHook'" class="small grey">({{action.options.url}})</span>
|
||||
<span v-if="actionIndex != threshold.actions.length - 1" style="font-style: italic">AND </span></span>
|
||||
|
||||
<a href="" title="修改" @click.prevent="update(index)"><i class="icon pencil small"></i></a>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon small remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新阈值 -->
|
||||
<div v-if="isAdding" style="margin-top: 0.5em">
|
||||
<table class="ui table celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 50%; background: #f9fafb; border-bottom: 1px solid rgba(34,36,38,.1)">阈值</td>
|
||||
<th>动作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td style="background: white">
|
||||
<!-- 已经添加的项目 -->
|
||||
<div>
|
||||
<div v-for="(item, index) in addingThreshold.items" class="ui label basic small" style="margin-bottom: 0.5em;">
|
||||
<span v-if="item.item != 'nodeHealthCheck'">
|
||||
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
|
||||
</span>
|
||||
{{itemName(item.item)}}
|
||||
|
||||
<span v-if="item.item == 'nodeHealthCheck'">
|
||||
<!-- 健康检查 -->
|
||||
<span v-if="item.value == 1">成功</span>
|
||||
<span v-if="item.value == 0">失败</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- 连通性 -->
|
||||
<span v-if="item.item == 'connectivity' && item.options != null && item.options.groups != null && item.options.groups.length > 0">[<span v-for="(group, groupIndex) in item.options.groups">{{group.name}} <span v-if="groupIndex != item.options.groups.length - 1"> </span></span>]</span>
|
||||
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> {{item.value}}{{itemUnitName(item.item)}}
|
||||
</span>
|
||||
|
||||
<a href="" title="删除" @click.prevent="removeItem(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正在添加的项目 -->
|
||||
<div v-if="isAddingItem" style="margin-top: 0.8em">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td style="width: 6em">统计项目</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="itemCode">
|
||||
<option v-for="item in allItems" :value="item.code">{{item.name}}</option>
|
||||
</select>
|
||||
<p class="comment" style="font-weight: normal" v-for="item in allItems" v-if="item.code == itemCode">{{item.description}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="itemCode != 'nodeHealthCheck'">
|
||||
<td>统计周期</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="itemDuration" style="width: 4em" maxlength="4" ref="itemDuration" @keyup.enter="confirmItem()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">分钟</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="itemCode != 'nodeHealthCheck'">
|
||||
<td>操作符</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="itemOperator">
|
||||
<option v-for="operator in allOperators" :value="operator.code">{{operator.name}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="itemCode != 'nodeHealthCheck'">
|
||||
<td>对比值</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" maxlength="20" style="width: 5em" v-model="itemValue" ref="itemValue" @keyup.enter="confirmItem()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label" v-for="item in allItems" v-if="item.code == itemCode">{{item.unit}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="itemCode == 'nodeHealthCheck'">
|
||||
<td>检查结果</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="itemValue">
|
||||
<option value="1">成功</option>
|
||||
<option value="0">失败</option>
|
||||
</select>
|
||||
<p class="comment" style="font-weight: normal">只有状态发生改变的时候才会触发。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 连通性 -->
|
||||
<tr v-if="itemCode == 'connectivity'">
|
||||
<td>终端分组</td>
|
||||
<td style="font-weight: normal">
|
||||
<div style="zoom: 0.8"><report-node-groups-selector @change="changeReportGroups"></report-node-groups-selector></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="margin-top: 0.8em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirmItem">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancelItem"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em" v-if="!isAddingItem">
|
||||
<button class="ui button tiny" type="button" @click.prevent="addItem">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="background: white">
|
||||
<!-- 已经添加的动作 -->
|
||||
<div>
|
||||
<div v-for="(action, index) in addingThreshold.actions" class="ui label basic small" style="margin-bottom: 0.5em">
|
||||
{{actionName(action.action)}}
|
||||
<span v-if="action.action == 'switch'">到{{action.options.ips.join(", ")}}</span>
|
||||
<span v-if="action.action == 'webHook'" class="small grey">({{action.options.url}})</span>
|
||||
<a href="" title="删除" @click.prevent="removeAction(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正在添加的动作 -->
|
||||
<div v-if="isAddingAction" style="margin-top: 0.8em">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td style="width: 6em">动作类型</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="actionCode">
|
||||
<option v-for="action in allActions" :value="action.code">{{action.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-for="action in allActions" v-if="action.code == actionCode">{{action.description}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 切换 -->
|
||||
<tr v-if="actionCode == 'switch'">
|
||||
<td>备用IP *</td>
|
||||
<td>
|
||||
<textarea rows="2" v-model="actionBackupIPs" ref="actionBackupIPs"></textarea>
|
||||
<p class="comment">每行一个备用IP。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- WebHook -->
|
||||
<tr v-if="actionCode == 'webHook'">
|
||||
<td>URL *</td>
|
||||
<td>
|
||||
<input type="text" maxlength="1000" placeholder="https://..." v-model="actionWebHookURL" ref="webHookURL" @keyup.enter="confirmAction()" @keypress.enter.prevent="1"/>
|
||||
<p class="comment">完整的URL,比如<code-label>https://example.com/webhook/api</code-label>,系统会在触发阈值的时候通过GET调用此URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="margin-top: 0.8em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirmAction">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancelAction"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 0.8em" v-if="!isAddingAction">
|
||||
<button class="ui button tiny" type="button" @click.prevent="addAction">+</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- 添加阈值 -->
|
||||
<div>
|
||||
<button class="ui button tiny" :class="{disabled: (isAddingItem || isAddingAction)}" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAdding" style="margin-top: 0.5em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
// 节点IP阈值
|
||||
Vue.component("node-ip-address-thresholds-view", {
|
||||
props: ["v-thresholds"],
|
||||
data: function () {
|
||||
let thresholds = this.vThresholds
|
||||
if (thresholds == null) {
|
||||
thresholds = []
|
||||
} else {
|
||||
thresholds.forEach(function (v) {
|
||||
if (v.items == null) {
|
||||
v.items = []
|
||||
}
|
||||
if (v.actions == null) {
|
||||
v.actions = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
thresholds: thresholds,
|
||||
allItems: window.IP_ADDR_THRESHOLD_ITEMS,
|
||||
allOperators: [
|
||||
{
|
||||
"name": "小于等于",
|
||||
"code": "lte"
|
||||
},
|
||||
{
|
||||
"name": "大于",
|
||||
"code": "gt"
|
||||
},
|
||||
{
|
||||
"name": "不等于",
|
||||
"code": "neq"
|
||||
},
|
||||
{
|
||||
"name": "小于",
|
||||
"code": "lt"
|
||||
},
|
||||
{
|
||||
"name": "大于等于",
|
||||
"code": "gte"
|
||||
}
|
||||
],
|
||||
allActions: window.IP_ADDR_THRESHOLD_ACTIONS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
itemName: function (item) {
|
||||
let result = ""
|
||||
this.allItems.forEach(function (v) {
|
||||
if (v.code == item) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
itemUnitName: function (itemCode) {
|
||||
let result = ""
|
||||
this.allItems.forEach(function (v) {
|
||||
if (v.code == itemCode) {
|
||||
result = v.unit
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
itemDurationUnitName: function (unit) {
|
||||
switch (unit) {
|
||||
case "minute":
|
||||
return "分钟"
|
||||
case "second":
|
||||
return "秒"
|
||||
case "hour":
|
||||
return "小时"
|
||||
case "day":
|
||||
return "天"
|
||||
}
|
||||
return unit
|
||||
},
|
||||
itemOperatorName: function (operator) {
|
||||
let result = ""
|
||||
this.allOperators.forEach(function (v) {
|
||||
if (v.code == operator) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
actionName: function (actionCode) {
|
||||
let result = ""
|
||||
this.allActions.forEach(function (v) {
|
||||
if (v.code == actionCode) {
|
||||
result = v.name
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<!-- 已有条件 -->
|
||||
<div v-if="thresholds.length > 0">
|
||||
<div class="ui label basic small" v-for="(threshold, index) in thresholds" style="margin-bottom: 0.8em">
|
||||
<span v-for="(item, itemIndex) in threshold.items">
|
||||
<span>
|
||||
<span v-if="item.item != 'nodeHealthCheck'">
|
||||
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
|
||||
</span>
|
||||
{{itemName(item.item)}}
|
||||
|
||||
<span v-if="item.item == 'nodeHealthCheck'">
|
||||
<!-- 健康检查 -->
|
||||
<span v-if="item.value == 1">成功</span>
|
||||
<span v-if="item.value == 0">失败</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- 连通性 -->
|
||||
<span v-if="item.item == 'connectivity' && item.options != null && item.options.groups != null && item.options.groups.length > 0">[<span v-for="(group, groupIndex) in item.options.groups">{{group.name}} <span v-if="groupIndex != item.options.groups.length - 1"> </span></span>]</span>
|
||||
|
||||
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> {{item.value}}{{itemUnitName(item.item)}}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="itemIndex != threshold.items.length - 1" style="font-style: italic">AND </span></span>
|
||||
->
|
||||
<span v-for="(action, actionIndex) in threshold.actions">{{actionName(action.action)}}
|
||||
<span v-if="action.action == 'switch'">到{{action.options.ips.join(", ")}}</span>
|
||||
<span v-if="action.action == 'webHook'" class="small grey">({{action.options.url}})</span>
|
||||
|
||||
<span v-if="actionIndex != threshold.actions.length - 1" style="font-style: italic">AND </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
// 节点IP地址管理(标签形式)
|
||||
Vue.component("node-ip-addresses-box", {
|
||||
props: ["v-ip-addresses", "role", "v-node-id"],
|
||||
data: function () {
|
||||
let nodeId = this.vNodeId
|
||||
if (nodeId == null) {
|
||||
nodeId = 0
|
||||
}
|
||||
|
||||
return {
|
||||
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
|
||||
supportThresholds: this.role != "ns",
|
||||
nodeId: nodeId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 添加IP地址
|
||||
addIPAddress: function () {
|
||||
window.UPDATING_NODE_IP_ADDRESS = null
|
||||
|
||||
let that = this;
|
||||
teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
||||
callback: function (resp) {
|
||||
that.ipAddresses.push(resp.data.ipAddress);
|
||||
},
|
||||
height: "24em",
|
||||
width: "44em"
|
||||
})
|
||||
},
|
||||
|
||||
// 修改地址
|
||||
updateIPAddress: function (index, address) {
|
||||
window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address)
|
||||
|
||||
let that = this;
|
||||
teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
||||
callback: function (resp) {
|
||||
Vue.set(that.ipAddresses, index, resp.data.ipAddress);
|
||||
},
|
||||
height: "24em",
|
||||
width: "44em"
|
||||
})
|
||||
},
|
||||
|
||||
// 删除IP地址
|
||||
removeIPAddress: function (index) {
|
||||
this.ipAddresses.$remove(index);
|
||||
},
|
||||
|
||||
// 判断是否为IPv6
|
||||
isIPv6: function (ip) {
|
||||
return ip.indexOf(":") > -1
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="ipAddressesJSON" :value="JSON.stringify(ipAddresses)"/>
|
||||
<div v-if="ipAddresses.length > 0">
|
||||
<div>
|
||||
<div v-for="(address, index) in ipAddresses" class="ui label tiny basic">
|
||||
<span v-if="isIPv6(address.ip)" class="grey">[IPv6]</span> {{address.ip}}
|
||||
<span class="small grey" v-if="address.name.length > 0">(备注:{{address.name}}<span v-if="!address.canAccess">,不公开访问</span>)</span>
|
||||
<span class="small grey" v-if="address.name.length == 0 && !address.canAccess">(不公开访问)</span>
|
||||
<span class="small red" v-if="!address.isOn" title="未启用">[off]</span>
|
||||
<span class="small red" v-if="!address.isUp" title="已下线">[down]</span>
|
||||
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[{{address.thresholds.length}}个阈值]</span>
|
||||
|
||||
<span v-if="address.clusters != null && address.clusters.length > 0">
|
||||
<span class="small grey">专属集群:[</span><span v-for="(cluster, index) in address.clusters" class="small grey">{{cluster.name}}<span v-if="index < address.clusters.length - 1">,</span></span><span class="small grey">]</span>
|
||||
|
||||
</span>
|
||||
|
||||
<a href="" title="修改" @click.prevent="updateIPAddress(index, address)"><i class="icon pencil small"></i></a>
|
||||
<a href="" title="删除" @click.prevent="removeIPAddress(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button small" type="button" @click.prevent="addIPAddress()">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
// 节点级别选择器
|
||||
Vue.component("node-level-selector", {
|
||||
props: ["v-node-level"],
|
||||
data: function () {
|
||||
let levelCode = this.vNodeLevel
|
||||
if (levelCode == null || levelCode < 1) {
|
||||
levelCode = 1
|
||||
}
|
||||
return {
|
||||
levels: [
|
||||
{
|
||||
name: "边缘节点",
|
||||
code: 1,
|
||||
description: "普通的边缘节点。"
|
||||
},
|
||||
{
|
||||
name: "L2节点",
|
||||
code: 2,
|
||||
description: "特殊的边缘节点,同时负责同组上一级节点的回源。"
|
||||
}
|
||||
],
|
||||
levelCode: levelCode
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
levelCode: function (code) {
|
||||
this.$emit("change", code)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<select class="ui dropdown auto-width" name="level" v-model="levelCode">
|
||||
<option v-for="level in levels" :value="level.code">{{level.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-if="typeof(levels[levelCode - 1]) != null"><plus-label
|
||||
></plus-label>{{levels[levelCode - 1].description}}</p>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
// 节点登录推荐端口
|
||||
Vue.component("node-login-suggest-ports", {
|
||||
data: function () {
|
||||
return {
|
||||
ports: [],
|
||||
availablePorts: [],
|
||||
autoSelected: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reload: function (host) {
|
||||
let that = this
|
||||
this.autoSelected = false
|
||||
this.isLoading = true
|
||||
Tea.action("/clusters/cluster/suggestLoginPorts")
|
||||
.params({
|
||||
host: host
|
||||
})
|
||||
.success(function (resp) {
|
||||
if (resp.data.availablePorts != null) {
|
||||
that.availablePorts = resp.data.availablePorts
|
||||
if (that.availablePorts.length > 0) {
|
||||
that.autoSelectPort(that.availablePorts[0])
|
||||
that.autoSelected = true
|
||||
}
|
||||
}
|
||||
if (resp.data.ports != null) {
|
||||
that.ports = resp.data.ports
|
||||
if (that.ports.length > 0 && !that.autoSelected) {
|
||||
that.autoSelectPort(that.ports[0])
|
||||
that.autoSelected = true
|
||||
}
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
that.isLoading = false
|
||||
})
|
||||
.post()
|
||||
},
|
||||
selectPort: function (port) {
|
||||
this.$emit("select", port)
|
||||
},
|
||||
autoSelectPort: function (port) {
|
||||
this.$emit("auto-select", port)
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
<span v-if="isLoading">正在检查端口...</span>
|
||||
<span v-if="availablePorts.length > 0">
|
||||
可能端口:<a href="" v-for="port in availablePorts" @click.prevent="selectPort(port)" class="ui label tiny basic blue" style="border: 1px #2185d0 dashed; font-weight: normal">{{port}}</a>
|
||||
|
||||
</span>
|
||||
<span v-if="ports.length > 0">
|
||||
常用端口:<a href="" v-for="port in ports" @click.prevent="selectPort(port)" class="ui label tiny basic blue" style="border: 1px #2185d0 dashed; font-weight: normal">{{port}}</a>
|
||||
</span>
|
||||
<span v-if="ports.length == 0">常用端口有22等。</span>
|
||||
<span v-if="ports.length > 0" class="grey small">(可以点击要使用的端口)</span>
|
||||
</span>`
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
Vue.component("node-region-selector", {
|
||||
props: ["v-region"],
|
||||
data: function () {
|
||||
return {
|
||||
selectedRegion: this.vRegion
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectRegion: function () {
|
||||
let that = this
|
||||
teaweb.popup("/clusters/regions/selectPopup?clusterId=" + this.vClusterId, {
|
||||
callback: function (resp) {
|
||||
that.selectedRegion = resp.data.region
|
||||
}
|
||||
})
|
||||
},
|
||||
addRegion: function () {
|
||||
let that = this
|
||||
teaweb.popup("/clusters/regions/createPopup?clusterId=" + this.vClusterId, {
|
||||
callback: function (resp) {
|
||||
that.selectedRegion = resp.data.region
|
||||
}
|
||||
})
|
||||
},
|
||||
removeRegion: function () {
|
||||
this.selectedRegion = null
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui label small basic" v-if="selectedRegion != null">
|
||||
<input type="hidden" name="regionId" :value="selectedRegion.id"/>
|
||||
{{selectedRegion.name}} <a href="" title="删除" @click.prevent="removeRegion()"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div v-if="selectedRegion == null">
|
||||
<a href="" @click.prevent="selectRegion()">[选择区域]</a> <a href="" @click.prevent="addRegion()">[添加区域]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
Vue.component("node-schedule-action-box", {
|
||||
props: ["value", "v-actions"],
|
||||
data: function () {
|
||||
let actionConfig = this.value
|
||||
if (actionConfig == null) {
|
||||
actionConfig = {
|
||||
code: "",
|
||||
params: {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
actions: this.vActions,
|
||||
currentAction: null,
|
||||
actionConfig: actionConfig
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"actionConfig.code": function (actionCode) {
|
||||
if (actionCode.length == 0) {
|
||||
this.currentAction = null
|
||||
} else {
|
||||
this.currentAction = this.actions.$find(function (k, v) {
|
||||
return v.code == actionCode
|
||||
})
|
||||
}
|
||||
this.actionConfig.params = {}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="actionJSON" :value="JSON.stringify(actionConfig)"/>
|
||||
<div>
|
||||
<div>
|
||||
<select class="ui dropdown auto-width" v-model="actionConfig.code">
|
||||
<option value="">[选择动作]</option>
|
||||
<option v-for="action in actions" :value="action.code">{{action.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="comment" v-if="currentAction != null">{{currentAction.description}}</p>
|
||||
|
||||
<div v-if="actionConfig.code == 'webHook'">
|
||||
<input type="text" placeholder="https://..." v-model="actionConfig.params.url"/>
|
||||
<p class="comment">接收通知的URL。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user