Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View 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>`
})

View File

@@ -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>
&nbsp; <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>`
})

View File

@@ -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>`
})

View File

@@ -0,0 +1,9 @@
Vue.component("api-node-selector", {
props: [],
data: function () {
return {}
},
template: `<div>
暂未实现
</div>`
})

View File

@@ -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>`
})

View File

@@ -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>
&nbsp;<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>`
})

View File

@@ -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>
&nbsp;<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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View File

@@ -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}} &nbsp; <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> &nbsp; <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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View 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>`
})

View 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>`
})

View File

@@ -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>`
})

View 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>`
})

View 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>`
})

View 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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View 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"/>`
})

View 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>`
})

View 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)"> &nbsp;1小时&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(1)"> &nbsp;1天&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(3)"> &nbsp;3天&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(7)"> &nbsp;1周&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(30)"> &nbsp;30天&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextYear()"> &nbsp;1年&nbsp; </a> </p>
</div>`
})

View File

@@ -0,0 +1,3 @@
Vue.component("dot", {
template: '<span style="display: inline-block; padding-bottom: 3px"><i class="icon circle tiny"></i></span>'
})

View 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>`,
})

View 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>`
})

View 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>'
});

View File

@@ -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> &nbsp; <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>`
})

View 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> \
'
});

View 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>`
});

View 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"/>`
})

View 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>`
})

View 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, "&amp;")
s = s.replace(/</g, "&lt;")
s = s.replace(/>/g, "&gt;")
s = s.replace(/"/g, "&quot;")
return s
}
},
template: `<span><span style="display: none"><slot></slot></span><span v-html="text"></span></span>`
})

View File

@@ -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>'
});

View 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>`
})

View 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>&nbsp;<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>&nbsp;<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)
}

View File

@@ -0,0 +1,3 @@
Vue.component("mask-warning", {
template: `<span class="red">为了安全起见,此项数据保存后将不允许在界面查看完整明文,为避免忘记,请自行记录原始数据。</span>`
})

View 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> \
'
});

View File

@@ -0,0 +1,5 @@
Vue.component("loading-message", {
template: `<div class="ui message loading">
<div class="ui active inline loader small"></div> &nbsp; <slot></slot>
</div>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View File

@@ -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>'
});

View File

@@ -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>`
})

View File

@@ -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>
&nbsp; &nbsp; <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>`
})

View File

@@ -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>`
})

View 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> &nbsp; <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>`
})

View 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>`
})

View File

@@ -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>`
})

View 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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View 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>`
})

View File

@@ -0,0 +1,3 @@
Vue.component("raquo-item", {
template: `<span class="item disabled" style="padding: 0">&raquo;</span>`
})

View File

@@ -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>`
})

View 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>`
})

View 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>'
});

View File

@@ -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>`
})

View File

@@ -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>`
})

View 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">&nbsp; <i class="ui icon long arrow small" :class="{down: order == 'asc', up: order == 'desc', 'down grey': order == '' || order == null}"></i></a>`
})

View 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)
}

View 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>`
})

View File

@@ -0,0 +1,6 @@
/**
* 保存按钮
*/
Vue.component("submit-btn", {
template: '<button class="ui button primary" type="submit"><slot>保存</slot></button>'
});

View 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>`
})

View 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>`
})

View 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> &nbsp;
<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>`
})

View 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"/>
&nbsp; <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>`
});

View File

@@ -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>`
})

View File

@@ -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}} &raquo; </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>`
})

View File

@@ -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>`
})

View File

@@ -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>
&nbsp;
<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>
&nbsp; <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>`
})

View 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> &nbsp; <a href="" @click.prevent="cancel()"><i class="icon remove small"></i></a>
</div>
</div>`
})

View File

@@ -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>`
})

View 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> &nbsp; &nbsp; <a href="" @click.prevent="create()">[添加新认证]</a>
</div>
</div>`
})

View 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>`
})

View 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">&nbsp; 级别:{{vItem.eventLevelName}}</span>
</span>`
})

View File

@@ -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> &nbsp; <span v-if="lists.length > 0"><span class="disabled small">|&nbsp;</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>`
})

View 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>
&nbsp; &nbsp;
<button class="ui button basic" type="button" @click.prevent="deleteCount" v-if="vTotal != null && vTotal >= MaxDeletes">批量删除所有搜索结果({{MaxDeletes}}个)</button>
&nbsp; &nbsp;
<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">&nbsp;New&nbsp;</span>&nbsp;<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}} &raquo; {{item.sourceGroup.name}} &raquo; {{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}} &raquo; {{item.sourceGroup.name}} &raquo; {{item.sourceSet.name}}</span></a>
</div>
</td>
<td>
<a href="" @click.prevent="viewLogs(item.id)">日志</a> &nbsp;
<a href="" @click.prevent="updateItem(item.id)">修改</a> &nbsp;
<a href="" @click.prevent="deleteItem(item.id)">删除</a>
</td>
</tr>
</tbody>
</table>
</div>`
})

View 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">国家/地区排行&nbsp; <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>`
})

View File

@@ -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>`
})

View File

@@ -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>`
})

View File

@@ -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> &nbsp; <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>`
})

View File

@@ -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}} &nbsp; <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>`
})

View 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">查看问题 &raquo;</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"> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" target="_top">查看任务&raquo;</a></span>
</div>
<!-- 证书续期成功 -->
<div v-if="message.type == 'SSLCertACMETaskSuccess'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</a>
</div>
<!-- 证书续期失败 -->
<div v-if="message.type == 'SSLCertACMETaskFailed'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</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">查看调度状态 &raquo;</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">查看详情 &raquo;</a>
</div>
</td>
</tr>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -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}} &nbsp; <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>
&nbsp; <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>`
})

View 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>`
})

View File

@@ -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}} &nbsp;<a href="" title="删除" @click.prevent="removeGroup()"><i class="icon remove"></i></a>
</div>
<div v-if="selectedGroup == null">
<a href="" @click.prevent="selectGroup()">[选择分组]</a> &nbsp; <a href="" @click.prevent="addGroup()">[添加分组]</a>
</div>
</div>`
})

View File

@@ -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">默认用于所有集群 &nbsp; <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> &nbsp; <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>`
})

View File

@@ -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">&nbsp; </span></span>]</span>
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> &nbsp;{{item.value}}{{itemUnitName(item.item)}}
</span>
&nbsp;<span v-if="itemIndex != threshold.items.length - 1" style="font-style: italic">AND &nbsp;</span>
</span>
-&gt;
<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>
&nbsp;<span v-if="actionIndex != threshold.actions.length - 1" style="font-style: italic">AND &nbsp;</span></span>
&nbsp;
<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">&nbsp; </span></span>]</span>
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> {{item.value}}{{itemUnitName(item.item)}}
</span>
&nbsp;
<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> &nbsp;
<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)}} &nbsp;
<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> &nbsp;
<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> &nbsp;
<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>`
})

View File

@@ -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">&nbsp; </span></span>]</span>
<span class="grey">[{{itemOperatorName(item.operator)}}]</span> {{item.value}}{{itemUnitName(item.item)}} &nbsp;
</span>
</span>
<span v-if="itemIndex != threshold.items.length - 1" style="font-style: italic">AND &nbsp;</span></span>
-&gt;
<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>
&nbsp;
<span v-if="actionIndex != threshold.actions.length - 1" style="font-style: italic">AND &nbsp;</span>
</span>
</div>
</div>
</div>`
})

View File

@@ -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>
&nbsp;
<span v-if="address.clusters != null && address.clusters.length > 0">
&nbsp; <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>
&nbsp;
</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>`
})

View File

@@ -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>`
})

View File

@@ -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>
&nbsp; &nbsp;
</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>`
})

View File

@@ -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}} &nbsp;<a href="" title="删除" @click.prevent="removeRegion()"><i class="icon remove"></i></a>
</div>
<div v-if="selectedRegion == null">
<a href="" @click.prevent="selectRegion()">[选择区域]</a> &nbsp; <a href="" @click.prevent="addRegion()">[添加区域]</a>
</div>
</div>`
})

View File

@@ -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>`
})

View File

@@ -0,0 +1,326 @@
Vue.component("node-schedule-conds-box", {
props: ["value", "v-params", "v-operators"],
mounted: function () {
this.formatConds(this.condsConfig.conds)
this.$forceUpdate()
},
data: function () {
let condsConfig = this.value
if (condsConfig == null) {
condsConfig = {
conds: [],
connector: "and"
}
}
if (condsConfig.conds == null) {
condsConfig.conds = []
}
let paramMap = {}
this.vParams.forEach(function (param) {
paramMap[param.code] = param
})
let operatorMap = {}
this.vOperators.forEach(function (operator) {
operatorMap[operator.code] = operator.name
})
return {
condsConfig: condsConfig,
params: this.vParams,
paramMap: paramMap,
operatorMap: operatorMap,
operator: "",
isAdding: false,
paramCode: "",
param: null,
valueBandwidth: {
count: 100,
unit: "mb"
},
valueTraffic: {
count: 1,
unit: "gb"
},
valueCPU: 80,
valueMemory: 90,
valueLoad: 20,
valueRate: 0
}
},
watch: {
paramCode: function (code) {
if (code.length == 0) {
this.param = null
} else {
this.param = this.params.$find(function (k, v) {
return v.code == code
})
}
this.$emit("changeparam", this.param)
}
},
methods: {
add: function () {
this.isAdding = true
},
confirm: function () {
if (this.param == null) {
teaweb.warn("请选择参数")
return
}
if (this.param.operators != null && this.param.operators.length > 0 && this.operator.length == 0) {
teaweb.warn("请选择操作符")
return
}
if (this.param.operators == null || this.param.operators.length == 0) {
this.operator = ""
}
let value = null
switch (this.param.valueType) {
case "bandwidth": {
if (this.valueBandwidth.unit.length == 0) {
teaweb.warn("请选择带宽单位")
return
}
let count = parseInt(this.valueBandwidth.count.toString())
if (isNaN(count)) {
count = 0
}
if (count < 0) {
count = 0
}
value = {
count: count,
unit: this.valueBandwidth.unit
}
}
break
case "traffic": {
if (this.valueTraffic.unit.length == 0) {
teaweb.warn("请选择带宽单位")
return
}
let count = parseInt(this.valueTraffic.count.toString())
if (isNaN(count)) {
count = 0
}
if (count < 0) {
count = 0
}
value = {
count: count,
unit: this.valueTraffic.unit
}
}
break
case "cpu":
let cpu = parseInt(this.valueCPU.toString())
if (isNaN(cpu)) {
cpu = 0
}
if (cpu < 0) {
cpu = 0
}
if (cpu > 100) {
cpu = 100
}
value = cpu
break
case "memory":
let memory = parseInt(this.valueMemory.toString())
if (isNaN(memory)) {
memory = 0
}
if (memory < 0) {
memory = 0
}
if (memory > 100) {
memory = 100
}
value = memory
break
case "load":
let load = parseInt(this.valueLoad.toString())
if (isNaN(load)) {
load = 0
}
if (load < 0) {
load = 0
}
value = load
break
case "rate":
let rate = parseInt(this.valueRate.toString())
if (isNaN(rate)) {
rate = 0
}
if (rate < 0) {
rate = 0
}
value = rate
break
}
this.condsConfig.conds.push({
param: this.param.code,
operator: this.operator,
value: value
})
this.formatConds(this.condsConfig.conds)
this.cancel()
},
cancel: function () {
this.isAdding = false
this.paramCode = ""
this.param = null
},
remove: function (index) {
this.condsConfig.conds.$remove(index)
},
formatConds: function (conds) {
let that = this
conds.forEach(function (cond) {
switch (that.paramMap[cond.param].valueType) {
case "bandwidth":
cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps"
return
case "traffic":
cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase()
return
case "cpu":
cond.valueFormat = cond.value + "%"
return
case "memory":
cond.valueFormat = cond.value + "%"
return
case "load":
cond.valueFormat = cond.value
return
case "rate":
cond.valueFormat = cond.value + "/秒"
return
}
})
}
},
template: `<div>
<input type="hidden" name="condsJSON" :value="JSON.stringify(this.condsConfig)"/>
<!-- 已有条件 -->
<div v-if="condsConfig.conds.length > 0" style="margin-bottom: 1em">
<span v-for="(cond, index) in condsConfig.conds">
<span class="ui label basic small">
<span>{{paramMap[cond.param].name}}
<span v-if="paramMap[cond.param].operators != null && paramMap[cond.param].operators.length > 0"><span class="grey">{{operatorMap[cond.operator]}}</span> {{cond.valueFormat}}</span>
&nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</span>
</span>
<span v-if="index < condsConfig.conds.length - 1"> &nbsp;<span v-if="condsConfig.connector == 'and'">且</span><span v-else>或</span>&nbsp; </span>
</span>
</div>
<div v-if="isAdding">
<table class="ui table">
<tbody>
<tr>
<td class="title">参数</td>
<td>
<select class="ui dropdown auto-width" v-model="paramCode">
<option value="">[选择参数]</option>
<option v-for="paramOption in params" :value="paramOption.code">{{paramOption.name}}</option>
</select>
<p class="comment" v-if="param != null">{{param.description}}</p>
</td>
</tr>
<tr v-if="param != null && param.operators != null && param.operators.length > 0">
<td>操作符</td>
<td>
<select class="ui dropdown auto-width" v-if="param != null" v-model="operator">
<option value="">[选择操作符]</option>
<option v-for="operator in param.operators" :value="operator">{{operatorMap[operator]}}</option>
</select>
</td>
</tr>
<tr v-if="param != null && param.operators != null && param.operators.length > 0">
<td>{{param.valueName}}</td>
<td>
<!-- 带宽 -->
<div v-if="param.valueType == 'bandwidth'">
<div class="ui fields inline">
<div class="ui field">
<input type="text" maxlength="10" size="6" v-model="valueBandwidth.count" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<select class="ui dropdown auto-width" v-model="valueBandwidth.unit">
<option value="gb">Gbps</option>
<option value="mb">Mbps</option>
</select>
</div>
</div>
</div>
<!-- 流量 -->
<div v-if="param.valueType == 'traffic'">
<div class="ui fields inline">
<div class="ui field">
<input type="text" maxlength="10" size="6" v-model="valueTraffic.count" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<select class="ui dropdown auto-width" v-model="valueTraffic.unit">
<option value="mb">MiB</option>
<option value="gb">GiB</option>
<option value="tb">TiB</option>
<option value="pb">PiB</option>
<option value="eb">EiB</option>
</select>
</div>
</div>
</div>
<!-- cpu -->
<div v-if="param.valueType == 'cpu'">
<div class="ui input right labeled">
<input type="text" v-model="valueCPU" maxlength="3" size="3" style="width: 4em" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
<span class="ui label">%</span>
</div>
</div>
<!-- memory -->
<div v-if="param.valueType == 'memory'">
<div class="ui input right labeled">
<input type="text" v-model="valueMemory" maxlength="3" size="3" style="width: 4em" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
<span class="ui label">%</span>
</div>
</div>
<!-- load -->
<div v-if="param.valueType == 'load'">
<input type="text" v-model="valueLoad" maxlength="3" size="3" style="width: 4em" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
<!-- rate -->
<div v-if="param.valueType == 'rate'">
<div class="ui input right labeled">
<input type="text" v-model="valueRate" maxlength="8" size="8" style="width: 8em" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
<span class="ui label">/秒</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<button class="ui button small" type="button" @click.prevent="confirm">确定</button> &nbsp; <a href="" @click.prevent="cancel">取消</a>
</div>
<div v-if="!isAdding">
<button class="ui button small" type="button" @click.prevent="add">+</button>
</div>
</div>`
})

View File

@@ -0,0 +1,61 @@
Vue.component("node-schedule-conds-viewer", {
props: ["value", "v-params", "v-operators"],
mounted: function () {
this.formatConds(this.condsConfig.conds)
this.$forceUpdate()
},
data: function () {
let paramMap = {}
this.vParams.forEach(function (param) {
paramMap[param.code] = param
})
let operatorMap = {}
this.vOperators.forEach(function (operator) {
operatorMap[operator.code] = operator.name
})
return {
condsConfig: this.value,
paramMap: paramMap,
operatorMap: operatorMap
}
},
methods: {
formatConds: function (conds) {
let that = this
conds.forEach(function (cond) {
switch (that.paramMap[cond.param].valueType) {
case "bandwidth":
cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps"
return
case "traffic":
cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase()
return
case "cpu":
cond.valueFormat = cond.value + "%"
return
case "memory":
cond.valueFormat = cond.value + "%"
return
case "load":
cond.valueFormat = cond.value
return
case "rate":
cond.valueFormat = cond.value + "/秒"
return
}
})
}
},
template: `<div>
<span v-for="(cond, index) in condsConfig.conds">
<span class="ui label basic small">
<span>{{paramMap[cond.param].name}}
<span v-if="paramMap[cond.param].operators != null && paramMap[cond.param].operators.length > 0"><span class="grey">{{operatorMap[cond.operator]}}</span> {{cond.valueFormat}}</span>
</span>
</span>
<span v-if="index < condsConfig.conds.length - 1"> &nbsp;<span v-if="condsConfig.connector == 'and'">且</span><span v-else>或</span>&nbsp; </span>
</span>
</div>`
})

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
Vue.component("ns-cluster-combo-box", {
props: ["v-cluster-id", "name"],
data: function () {
let that = this
Tea.action("/ns/clusters/options")
.post()
.success(function (resp) {
that.clusters = resp.data.clusters
})
let inputName = "clusterId"
if (this.name != null && this.name.length > 0) {
inputName = this.name
}
return {
clusters: [],
inputName: inputName
}
},
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="inputName" :v-value="vClusterId" @change="change"></combo-box>
</div>`
})

Some files were not shown because too many files have changed in this diff Show More