diff --git a/build/build.sh b/build/build.sh
index 70a090b5..2547bbae 100755
--- a/build/build.sh
+++ b/build/build.sh
@@ -58,6 +58,10 @@ function build() {
 	rm -f $(basename $EDGE_API_ZIP_FILE)
 	cd -
 
+    # generate files
+    echo "generating files ..."
+	go run  -tags $TAG $ROOT/../cmd/edge-admin/main.go generate
+
 	# build
 	echo "building "${NAME}" ..."
 	env GOOS=$OS GOARCH=$ARCH go build -tags $TAG -ldflags="-s -w" -o $DIST/bin/${NAME} $ROOT/../cmd/edge-admin/main.go
diff --git a/cmd/edge-admin/main.go b/cmd/edge-admin/main.go
index 6ec7eca2..df293f98 100644
--- a/cmd/edge-admin/main.go
+++ b/cmd/edge-admin/main.go
@@ -5,6 +5,7 @@ import (
 	"github.com/TeaOSLab/EdgeAdmin/internal/apps"
 	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
+	"github.com/TeaOSLab/EdgeAdmin/internal/gen"
 	"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
 	_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
 	_ "github.com/iwind/TeaGo/bootstrap"
@@ -70,6 +71,13 @@ func main() {
 		}
 		fmt.Println("change demo mode successfully")
 	})
+	app.On("generate", func() {
+		err := gen.Generate()
+		if err != nil {
+			fmt.Println("generate failed: " + err.Error())
+			return
+		}
+	})
 	app.Run(func() {
 		adminNode := nodes.NewAdminNode()
 		adminNode.Run()
diff --git a/internal/gen/generate.go b/internal/gen/generate.go
new file mode 100644
index 00000000..54a5476f
--- /dev/null
+++ b/internal/gen/generate.go
@@ -0,0 +1,129 @@
+// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
+
+package gen
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/conds/condutils"
+	"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
+	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
+	"github.com/iwind/TeaGo/Tea"
+	"github.com/iwind/TeaGo/files"
+	"github.com/iwind/TeaGo/logs"
+	"io"
+	"os"
+	"path/filepath"
+)
+
+func Generate() error {
+	err := generateComponentsJSFile()
+	if err != nil {
+		return errors.New("generate 'components.js' failed: " + err.Error())
+	}
+
+	return nil
+}
+
+// 生成Javascript文件
+func generateComponentsJSFile() error {
+	var buffer = bytes.NewBuffer([]byte{})
+
+	var webRoot string
+	if Tea.IsTesting() {
+		webRoot = Tea.Root + "/../web/public/js/components/"
+	} else {
+		webRoot = Tea.Root + "/web/public/js/components/"
+	}
+	f := files.NewFile(webRoot)
+
+	f.Range(func(file *files.File) {
+		if !file.IsFile() {
+			return
+		}
+		if file.Ext() != ".js" {
+			return
+		}
+		data, err := file.ReadAll()
+		if err != nil {
+			logs.Error(err)
+			return
+		}
+		buffer.Write(data)
+		buffer.Write([]byte{'\n', '\n'})
+	})
+
+	// 条件组件
+	typesJSON, err := json.Marshal(condutils.ReadAllAvailableCondTypes())
+	if err != nil {
+		logs.Println("ComponentsAction marshal request cond types failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.REQUEST_COND_COMPONENTS = ")
+		buffer.Write(typesJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	// 条件操作符
+	requestOperatorsJSON, err := json.Marshal(shared.AllRequestOperators())
+	if err != nil {
+		logs.Println("ComponentsAction marshal request operators failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.REQUEST_COND_OPERATORS = ")
+		buffer.Write(requestOperatorsJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	// 请求变量
+	requestVariablesJSON, err := json.Marshal(shared.DefaultRequestVariables())
+	if err != nil {
+		logs.Println("ComponentsAction marshal request variables failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.REQUEST_VARIABLES = ")
+		buffer.Write(requestVariablesJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	// 指标
+	metricHTTPKeysJSON, err := json.Marshal(serverconfigs.FindAllMetricKeyDefinitions(serverconfigs.MetricItemCategoryHTTP))
+	if err != nil {
+		logs.Println("ComponentsAction marshal metric http keys failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.METRIC_HTTP_KEYS = ")
+		buffer.Write(metricHTTPKeysJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	// IP地址阈值项目
+	ipAddrThresholdItemsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdItems())
+	if err != nil {
+		logs.Println("ComponentsAction marshal ip addr threshold items failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.IP_ADDR_THRESHOLD_ITEMS = ")
+		buffer.Write(ipAddrThresholdItemsJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	// IP地址阈值动作
+	ipAddrThresholdActionsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdActions())
+	if err != nil {
+		logs.Println("ComponentsAction marshal ip addr threshold actions failed: " + err.Error())
+	} else {
+		buffer.WriteString("window.IP_ADDR_THRESHOLD_ACTIONS = ")
+		buffer.Write(ipAddrThresholdActionsJSON)
+		buffer.Write([]byte{'\n', '\n'})
+	}
+
+	fp, err := os.OpenFile(filepath.Clean(Tea.PublicFile("/js/components.js")), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(fp, buffer)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/internal/gen/generate_test.go b/internal/gen/generate_test.go
new file mode 100644
index 00000000..8092964d
--- /dev/null
+++ b/internal/gen/generate_test.go
@@ -0,0 +1,13 @@
+// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
+
+package gen
+
+import "testing"
+
+func TestGenerate(t *testing.T) {
+	err := Generate()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log("ok")
+}
diff --git a/internal/web/actions/default/ui/init.go b/internal/web/actions/default/ui/init.go
index fc0543f6..390e9f8b 100644
--- a/internal/web/actions/default/ui/init.go
+++ b/internal/web/actions/default/ui/init.go
@@ -1,11 +1,10 @@
 package ui
 
 import (
-	"compress/gzip"
 	"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
 	"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
 	"github.com/iwind/TeaGo"
-	"github.com/iwind/TeaGo/actions"
+	"github.com/iwind/TeaGo/Tea"
 )
 
 func init() {
@@ -16,11 +15,6 @@ func init() {
 			// 公共可以访问的链接
 			Get("/image/:fileId", new(ImageAction)).
 
-			// 以下的需要压缩
-			Helper(&actions.Gzip{Level: gzip.BestCompression}).
-			Get("/components.js", new(ComponentsAction)).
-			EndHelpers().
-
 			// 以下需要登录
 			Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)).
 			Get("/download", new(DownloadAction)).
@@ -31,7 +25,13 @@ func init() {
 			Post("/hideTip", new(HideTipAction)).
 			Post("/theme", new(ThemeAction)).
 			Post("/validateIPs", new(ValidateIPsAction)).
-
 			EndAll()
+
+		// 开发环境下总是动态加载,以便于调试
+		if Tea.IsTesting() {
+			server.
+				Get("/js/components.js", new(ComponentsAction)).
+				EndAll()
+		}
 	})
 }
diff --git a/web/public/js/components.js b/web/public/js/components.js
new file mode 100755
index 00000000..ad11774f
--- /dev/null
+++ b/web/public/js/components.js
@@ -0,0 +1,12042 @@
+// 显示节点的多个集群
+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: `
`
+})
+
+// 单个集群选择
+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: `
+	
+		[选择集群] 
+		{{cluster.name}} 
+	 
+
`
+})
+
+// 一个节点的多个集群选择器
+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: `
+	
+	
+	
+		
+			主集群 
+			
+				
+				
+					+ 
+				
+				
+			 
+		 
+		
+			从集群 
+			
+				
+				
+					+ 
+				
+			 
+		 
+	
+
 `
+})
+
+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: `
+    
+        [选择媒介类型] 
+        {{media.name}} 
+     
+    
+
`
+})
+
+// 消息接收人设置
+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: `
+        
           
+        
+            
+               
分组: {{receiver.name}} 
({{receiver.subName}})    
+            
+             
+        
+      
+ 
+
 `
+})
+
+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: ``
+})
+
+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: `
+    
+        [选择媒介] 
+        {{instance.name}} ({{instance.media.name}}) 
+     
+    
+
`
+})
+
+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: ``
+})
+
+// 选择多个线路
+Vue.component("ns-routes-selector", {
+	props: ["v-routes"],
+	mounted: function () {
+		let that = this
+		Tea.action("/ns/routes/options")
+			.post()
+			.success(function (resp) {
+				that.routes = resp.data.routes
+			})
+	},
+	data: function () {
+		let selectedRoutes = this.vRoutes
+		if (selectedRoutes == null) {
+			selectedRoutes = []
+		}
+
+		return {
+			routeCode: "default",
+			routes: [],
+			isAdding: false,
+			routeType: "default",
+			selectedRoutes: selectedRoutes
+		}
+	},
+	watch: {
+		routeType: function (v) {
+			this.routeCode = ""
+			let that = this
+			this.routes.forEach(function (route) {
+				if (route.type == v && that.routeCode.length == 0) {
+					that.routeCode = route.code
+				}
+			})
+		}
+	},
+	methods: {
+		add: function () {
+			this.isAdding = true
+			this.routeType = "default"
+			this.routeCode = "default"
+		},
+		cancel: function () {
+			this.isAdding = false
+		},
+		confirm: function () {
+			if (this.routeCode.length == 0) {
+				return
+			}
+
+			let that = this
+			this.routes.forEach(function (v) {
+				if (v.code == that.routeCode) {
+					that.selectedRoutes.push(v)
+				}
+			})
+			this.cancel()
+		},
+		remove: function (index) {
+			this.selectedRoutes.$remove(index)
+		}
+	}
+	,
+	template: `
+	
+	
+		
+			
+				
+					[默认线路] 
+					自定义线路 
+					运营商 
+					中国省市 
+					全球国家地区 
+				 
+			
+			
+			
+				
+					{{route.name}} 
+				 
+			
+			
+			
+		
+	
+	
+ 
+
 `
+})
+
+// 递归DNS设置
+Vue.component("ns-recursion-config-box", {
+	props: ["v-recursion-config"],
+	data: function () {
+		let recursion = this.vRecursionConfig
+		if (recursion == null) {
+			recursion = {
+				isOn: false,
+				hosts: [],
+				allowDomains: [],
+				denyDomains: [],
+				useLocalHosts: false
+			}
+		}
+		if (recursion.hosts == null) {
+			recursion.hosts = []
+		}
+		if (recursion.allowDomains == null) {
+			recursion.allowDomains = []
+		}
+		if (recursion.denyDomains == null) {
+			recursion.denyDomains = []
+		}
+		return {
+			config: recursion,
+			hostIsAdding: false,
+			host: "",
+			updatingHost: null
+		}
+	},
+	methods: {
+		changeHosts: function (hosts) {
+			this.config.hosts = hosts
+		},
+		changeAllowDomains: function (domains) {
+			this.config.allowDomains = domains
+		},
+		changeDenyDomains: function (domains) {
+			this.config.denyDomains = domains
+		},
+		removeHost: function (index) {
+			this.config.hosts.$remove(index)
+		},
+		addHost: function () {
+			this.updatingHost = null
+			this.host = ""
+			this.hostIsAdding = !this.hostIsAdding
+			if (this.hostIsAdding) {
+				var that = this
+				setTimeout(function () {
+					let hostRef = that.$refs.hostRef
+					if (hostRef != null) {
+						hostRef.focus()
+					}
+				}, 200)
+			}
+		},
+		updateHost: function (host) {
+			this.updatingHost = host
+			this.host = host.host
+			this.hostIsAdding = !this.hostIsAdding
+
+			if (this.hostIsAdding) {
+				var that = this
+				setTimeout(function () {
+					let hostRef = that.$refs.hostRef
+					if (hostRef != null) {
+						hostRef.focus()
+					}
+				}, 200)
+			}
+		},
+		confirmHost: function () {
+			if (this.host.length == 0) {
+				teaweb.warn("请输入DNS地址")
+				return
+			}
+
+			// TODO 校验Host
+			// TODO 可以输入端口号
+			// TODO 可以选择协议
+
+			this.hostIsAdding = false
+			if (this.updatingHost == null) {
+				this.config.hosts.push({
+					host: this.host
+				})
+			} else {
+				this.updatingHost.host = this.host
+			}
+		},
+		cancelHost: function () {
+			this.hostIsAdding = false
+		}
+	},
+	template: ``
+})
+
+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: `
+	
+	
+		 
+		
+			
+				是否启用 
+				
+					 
+				 
+			 
+			
+				记录所有访问 
+				
+					 
+					
+				 
+			 
+		 
+	
+	
+
 `
+})
+
+Vue.component("ns-route-ranges-box", {
+	props: ["v-ranges"],
+	data: function () {
+		let ranges = this.vRanges
+		if (ranges == null) {
+			ranges = []
+		}
+		return {
+			ranges: ranges,
+			isAdding: false,
+
+			// IP范围
+			ipRangeFrom: "",
+			ipRangeTo: ""
+		}
+	},
+	methods: {
+		add: function () {
+			this.isAdding = true
+			let that = this
+			setTimeout(function () {
+				that.$refs.ipRangeFrom.focus()
+			}, 100)
+		},
+		remove: function (index) {
+			this.ranges.$remove(index)
+		},
+		cancelIPRange: function () {
+			this.isAdding = false
+			this.ipRangeFrom = ""
+			this.ipRangeTo = ""
+		},
+		confirmIPRange: function () {
+			// 校验IP
+			let that = this
+			this.ipRangeFrom = this.ipRangeFrom.trim()
+			if (!this.validateIP(this.ipRangeFrom)) {
+				teaweb.warn("开始IP填写错误", function () {
+					that.$refs.ipRangeFrom.focus()
+				})
+				return
+			}
+
+			this.ipRangeTo = this.ipRangeTo.trim()
+			if (!this.validateIP(this.ipRangeTo)) {
+				teaweb.warn("结束IP填写错误", function () {
+					that.$refs.ipRangeTo.focus()
+				})
+				return
+			}
+
+			this.ranges.push({
+				type: "ipRange",
+				params: {
+					ipFrom: this.ipRangeFrom,
+					ipTo: this.ipRangeTo
+				}
+			})
+			this.cancelIPRange()
+		},
+		validateIP: function (ip) {
+			if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
+				return false
+			}
+			let pieces = ip.split(".")
+			let isOk = true
+			pieces.forEach(function (v) {
+				let v1 = parseInt(v)
+				if (v1 > 255) {
+					isOk = false
+				}
+			})
+			return isOk
+		}
+	},
+	template: `
+	
+	
+		
+			
IP范围: 
+			{{range.params.ipFrom}} - {{range.params.ipTo}}   
+		
+		
+	
+	
+	
+	
+	
+	
+ 
+
 `
+})
+
+// 选择单一线路
+Vue.component("ns-route-selector", {
+	props: ["v-route-code"],
+	mounted: function () {
+		let that = this
+		Tea.action("/ns/routes/options")
+			.post()
+			.success(function (resp) {
+				that.routes = resp.data.routes
+			})
+	},
+	data: function () {
+		let routeCode = this.vRouteCode
+		if (routeCode == null) {
+			routeCode = ""
+		}
+		return {
+			routeCode: routeCode,
+			routes: []
+		}
+	},
+	template: `
+	
+		
+			[线路] 
+			{{route.name}} 
+		 
+	
+
 `
+})
+
+Vue.component("ns-user-selector", {
+	mounted: function () {
+		let that = this
+
+		Tea.action("/ns/users/options")
+			.post()
+			.success(function (resp) {
+				that.users = resp.data.users
+			})
+	},
+	props: ["v-user-id"],
+	data: function () {
+		let userId = this.vUserId
+		if (userId == null) {
+			userId = 0
+		}
+		return {
+			users: [],
+			userId: userId
+		}
+	},
+	template: `
+	
+		[选择用户] 
+		{{user.fullname}} ({{user.username}}) 
+	 
+
`
+})
+
+Vue.component("ns-access-log-box", {
+	props: ["v-access-log", "v-keyword"],
+	data: function () {
+		let accessLog = this.vAccessLog
+		return {
+			accessLog: accessLog
+		}
+	},
+	methods: {
+		showLog: function () {
+			let that = this
+			let requestId = this.accessLog.requestId
+			this.$parent.$children.forEach(function (v) {
+				if (v.deselect != null) {
+					v.deselect()
+				}
+			})
+			this.select()
+
+			teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, {
+				width: "50em",
+				height: "24em",
+				onClose: function () {
+					that.deselect()
+				}
+			})
+		},
+		select: function () {
+			this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
+		},
+		deselect: function () {
+			this.$refs.box.parentNode.style.cssText = ""
+		}
+	},
+	template: `
+	
[{{accessLog.region}}]  {{accessLog.remoteAddr}}  [{{accessLog.timeLocal}}] [{{accessLog.networking}}] 
{{accessLog.questionType}} {{accessLog.questionName}}   -> 
{{accessLog.recordType}} {{accessLog.recordValue}}  
+	
+		线路: {{route.name}} 
+		递归DNS 
+	
+	
+		 错误:[{{accessLog.error}}]
+	
+
 `
+})
+
+Vue.component("ns-cluster-selector", {
+	props: ["v-cluster-id"],
+	mounted: function () {
+		let that = this
+
+		Tea.action("/ns/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: `
+	
+		[选择集群] 
+		{{cluster.name}} 
+	 
+
`
+})
+
+Vue.component("plan-user-selector", {
+	mounted: function () {
+		let that = this
+
+		Tea.action("/plans/users/options")
+			.post()
+			.success(function (resp) {
+				that.users = resp.data.users
+			})
+	},
+	props: ["v-user-id"],
+	data: function () {
+		let userId = this.vUserId
+		if (userId == null) {
+			userId = 0
+		}
+		return {
+			users: [],
+			userId: userId
+		}
+	},
+	watch: {
+		userId: function (v) {
+			this.$emit("change", v)
+		}
+	},
+	template: `
+	
+		[选择用户] 
+		{{user.fullname}} ({{user.username}}) 
+	 
+
`
+})
+
+Vue.component("plan-price-view", {
+	props: ["v-plan"],
+	data: function () {
+		return {
+			plan: this.vPlan
+		}
+	},
+	template: `
+	 
+		月度:¥{{plan.monthlyPrice}}元 
+		季度:¥{{plan.seasonallyPrice}}元 
+		年度:¥{{plan.yearlyPrice}}元 
+	 
+	
+		基础价格:¥{{plan.trafficPrice.base}}元/GB
+	 
+
`
+})
+
+// 套餐价格配置
+Vue.component("plan-price-config-box", {
+	props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price"],
+	data: function () {
+		let priceType = this.vPriceType
+		if (priceType == null) {
+			priceType = "period"
+		}
+
+		let monthlyPriceNumber = 0
+		let monthlyPrice = this.vMonthlyPrice
+		if (monthlyPrice == null || monthlyPrice <= 0) {
+			monthlyPrice = ""
+		} else {
+			monthlyPrice = monthlyPrice.toString()
+			monthlyPriceNumber = parseFloat(monthlyPrice)
+			if (isNaN(monthlyPriceNumber)) {
+				monthlyPriceNumber = 0
+			}
+		}
+
+		let seasonallyPriceNumber = 0
+		let seasonallyPrice = this.vSeasonallyPrice
+		if (seasonallyPrice == null || seasonallyPrice <= 0) {
+			seasonallyPrice = ""
+		} else {
+			seasonallyPrice = seasonallyPrice.toString()
+			seasonallyPriceNumber = parseFloat(seasonallyPrice)
+			if (isNaN(seasonallyPriceNumber)) {
+				seasonallyPriceNumber = 0
+			}
+		}
+
+		let yearlyPriceNumber = 0
+		let yearlyPrice = this.vYearlyPrice
+		if (yearlyPrice == null || yearlyPrice <= 0) {
+			yearlyPrice = ""
+		} else {
+			yearlyPrice = yearlyPrice.toString()
+			yearlyPriceNumber = parseFloat(yearlyPrice)
+			if (isNaN(yearlyPriceNumber)) {
+				yearlyPriceNumber = 0
+			}
+		}
+
+		let trafficPrice = this.vTrafficPrice
+		let trafficPriceBaseNumber = 0
+		if (trafficPrice != null) {
+			trafficPriceBaseNumber = trafficPrice.base
+		} else {
+			trafficPrice = {
+				base: 0
+			}
+		}
+		let trafficPriceBase = ""
+		if (trafficPriceBaseNumber > 0) {
+			trafficPriceBase = trafficPriceBaseNumber.toString()
+		}
+
+		return {
+			priceType: priceType,
+			monthlyPrice: monthlyPrice,
+			seasonallyPrice: seasonallyPrice,
+			yearlyPrice: yearlyPrice,
+
+			monthlyPriceNumber: monthlyPriceNumber,
+			seasonallyPriceNumber: seasonallyPriceNumber,
+			yearlyPriceNumber: yearlyPriceNumber,
+
+			trafficPriceBase: trafficPriceBase,
+			trafficPrice: trafficPrice
+		}
+	},
+	watch: {
+		monthlyPrice: function (v) {
+			let price = parseFloat(v)
+			if (isNaN(price)) {
+				price = 0
+			}
+			this.monthlyPriceNumber = price
+		},
+		seasonallyPrice: function (v) {
+			let price = parseFloat(v)
+			if (isNaN(price)) {
+				price = 0
+			}
+			this.seasonallyPriceNumber = price
+		},
+		yearlyPrice: function (v) {
+			let price = parseFloat(v)
+			if (isNaN(price)) {
+				price = 0
+			}
+			this.yearlyPriceNumber = price
+		},
+		trafficPriceBase: function (v) {
+			let price = parseFloat(v)
+			if (isNaN(price)) {
+				price = 0
+			}
+			this.trafficPrice.base = price
+		}
+	},
+	template: `
+	
+	
+	
+	
+	
+	
+	
+		 按时间周期     
+		 按流量 
+	
+	
+	
+	
+	
+	
+	
+
 `
+})
+
+Vue.component("http-stat-config-box", {
+	props: ["v-stat-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let stat = this.vStatConfig
+		if (stat == null) {
+			stat = {
+				isPrior: false,
+				isOn: false
+			}
+		}
+		return {
+			stat: stat
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-request-conds-box", {
+	props: ["v-conds"],
+	data: function () {
+		let conds = this.vConds
+		if (conds == null) {
+			conds = {
+				isOn: true,
+				connector: "or",
+				groups: []
+			}
+		}
+		return {
+			conds: conds,
+			components: window.REQUEST_COND_COMPONENTS
+		}
+	},
+	methods: {
+		change: function () {
+			this.$emit("change", this.conds)
+		},
+		addGroup: function () {
+			window.UPDATING_COND_GROUP = null
+
+			let that = this
+			teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
+				height: "30em",
+				callback: function (resp) {
+					that.conds.groups.push(resp.data.group)
+					that.change()
+				}
+			})
+		},
+		updateGroup: function (groupIndex, group) {
+			window.UPDATING_COND_GROUP = group
+			let that = this
+			teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
+				height: "30em",
+				callback: function (resp) {
+					Vue.set(that.conds.groups, groupIndex, resp.data.group)
+					that.change()
+				}
+			})
+		},
+		removeGroup: function (groupIndex) {
+			let that = this
+			teaweb.confirm("确定要删除这一组条件吗?", function () {
+				that.conds.groups.$remove(groupIndex)
+				that.change()
+			})
+		},
+		typeName: function (cond) {
+			let c = this.components.$find(function (k, v) {
+				return v.type == cond.type
+			})
+			if (c != null) {
+				return c.name;
+			}
+			return cond.param + " " + cond.operator
+		}
+	},
+	template: `
+		
+		
+			
+				
+					分组{{groupIndex+1}} 
+					
+						
+							
+								{{cond.param}} {{cond.operator}}  
+								{{typeName(cond)}}:  
+								{{cond.value}}
+							 
+							
+							 {{group.connector}}   
+						 
+					 
+					
+						   
+					 
+				 
+			
+			
+		
+		
+		
+		
+			
+				分组之间关系 
+				
+					
+						和 
+						或 
+					 
+						
+				 
+			 
+		
+		
+		
+			+添加分组 
+		
+	
 	
+`
+})
+
+Vue.component("ssl-config-box", {
+	props: ["v-ssl-policy", "v-protocol", "v-server-id"],
+	created: function () {
+		let that = this
+		setTimeout(function () {
+			that.sortableCipherSuites()
+		}, 100)
+	},
+	data: function () {
+		let policy = this.vSslPolicy
+		if (policy == null) {
+			policy = {
+				id: 0,
+				isOn: true,
+				certRefs: [],
+				certs: [],
+				clientCARefs: [],
+				clientCACerts: [],
+				clientAuthType: 0,
+				minVersion: "TLS 1.1",
+				hsts: null,
+				cipherSuitesIsOn: false,
+				cipherSuites: [],
+				http2Enabled: true
+			}
+		} else {
+			if (policy.certRefs == null) {
+				policy.certRefs = []
+			}
+			if (policy.certs == null) {
+				policy.certs = []
+			}
+			if (policy.clientCARefs == null) {
+				policy.clientCARefs = []
+			}
+			if (policy.clientCACerts == null) {
+				policy.clientCACerts = []
+			}
+			if (policy.cipherSuites == null) {
+				policy.cipherSuites = []
+			}
+		}
+
+		let hsts = policy.hsts
+		if (hsts == null) {
+			hsts = {
+				isOn: false,
+				maxAge: 0,
+				includeSubDomains: false,
+				preload: false,
+				domains: []
+			}
+		}
+
+		return {
+			policy: policy,
+
+			// hsts
+			hsts: hsts,
+			hstsOptionsVisible: false,
+			hstsDomainAdding: false,
+			addingHstsDomain: "",
+			hstsDomainEditingIndex: -1,
+
+			// 相关数据
+			allVersions: window.SSL_ALL_VERSIONS,
+			allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(),
+			modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES,
+			intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES,
+			allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES,
+			cipherSuitesVisible: false,
+
+			// 高级选项
+			moreOptionsVisible: false
+		}
+	},
+	watch: {
+		hsts: {
+			deep: true,
+			handler: function () {
+				this.policy.hsts = this.hsts
+			}
+		}
+	},
+	methods: {
+		// 删除证书
+		removeCert: function (index) {
+			let that = this
+			teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
+				that.policy.certRefs.$remove(index)
+				that.policy.certs.$remove(index)
+			})
+		},
+
+		// 选择证书
+		selectCert: function () {
+			let that = this
+			let selectedCertIds = []
+			if (this.policy != null && this.policy.certs.length > 0) {
+				this.policy.certs.forEach(function (cert) {
+					selectedCertIds.push(cert.id.toString())
+				})
+			}
+			teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds, {
+				width: "50em",
+				height: "30em",
+				callback: function (resp) {
+					that.policy.certRefs.push(resp.data.certRef)
+					that.policy.certs.push(resp.data.cert)
+				}
+			})
+		},
+
+		// 上传证书
+		uploadCert: function () {
+			let that = this
+			teaweb.popup("/servers/certs/uploadPopup", {
+				height: "28em",
+				callback: function (resp) {
+					teaweb.success("上传成功", function () {
+						that.policy.certRefs.push(resp.data.certRef)
+						that.policy.certs.push(resp.data.cert)
+					})
+				}
+			})
+		},
+
+		// 申请证书
+		requestCert: function () {
+			// 已经在证书中的域名
+			let excludeServerNames = []
+			if (this.policy != null && this.policy.certs.length > 0) {
+				this.policy.certs.forEach(function (cert) {
+					excludeServerNames.$pushAll(cert.dnsNames)
+				})
+			}
+
+			let that = this
+			teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), {
+				callback: function () {
+					that.policy.certRefs.push(resp.data.certRef)
+					that.policy.certs.push(resp.data.cert)
+				}
+			})
+		},
+
+		// 更多选项
+		changeOptionsVisible: function () {
+			this.moreOptionsVisible = !this.moreOptionsVisible
+		},
+
+		// 格式化时间
+		formatTime: function (timestamp) {
+			return new Date(timestamp * 1000).format("Y-m-d")
+		},
+
+		// 格式化加密套件
+		formatCipherSuite: function (cipherSuite) {
+			return cipherSuite.replace(/(AES|3DES)/, "$1 ")
+		},
+
+		// 添加单个套件
+		addCipherSuite: function (cipherSuite) {
+			if (!this.policy.cipherSuites.$contains(cipherSuite)) {
+				this.policy.cipherSuites.push(cipherSuite)
+			}
+			this.allCipherSuites.$removeValue(cipherSuite)
+		},
+
+		// 删除单个套件
+		removeCipherSuite: function (cipherSuite) {
+			let that = this
+			teaweb.confirm("确定要删除此套件吗?", function () {
+				that.policy.cipherSuites.$removeValue(cipherSuite)
+				that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) {
+					return !that.policy.cipherSuites.$contains(v)
+				})
+			})
+		},
+
+		// 清除所选套件
+		clearCipherSuites: function () {
+			let that = this
+			teaweb.confirm("确定要清除所有已选套件吗?", function () {
+				that.policy.cipherSuites = []
+				that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy()
+			})
+		},
+
+		// 批量添加套件
+		addBatchCipherSuites: function (suites) {
+			var that = this
+			teaweb.confirm("确定要批量添加套件?", function () {
+				suites.$each(function (k, v) {
+					if (that.policy.cipherSuites.$contains(v)) {
+						return
+					}
+					that.policy.cipherSuites.push(v)
+				})
+			})
+		},
+
+		/**
+		 * 套件拖动排序
+		 */
+		sortableCipherSuites: function () {
+			var box = document.querySelector(".cipher-suites-box")
+			Sortable.create(box, {
+				draggable: ".label",
+				handle: ".icon.handle",
+				onStart: function () {
+
+				},
+				onUpdate: function (event) {
+
+				}
+			})
+		},
+
+		// 显示所有套件
+		showAllCipherSuites: function () {
+			this.cipherSuitesVisible = !this.cipherSuitesVisible
+		},
+
+		// 显示HSTS更多选项
+		showMoreHSTS: function () {
+			this.hstsOptionsVisible = !this.hstsOptionsVisible;
+			if (this.hstsOptionsVisible) {
+				this.changeHSTSMaxAge()
+			}
+		},
+
+		// 监控HSTS有效期修改
+		changeHSTSMaxAge: function () {
+			var v = this.hsts.maxAge
+			if (isNaN(v)) {
+				this.hsts.days = "-"
+				return
+			}
+			this.hsts.days = parseInt(v / 86400)
+			if (isNaN(this.hsts.days)) {
+				this.hsts.days = "-"
+			} else if (this.hsts.days < 0) {
+				this.hsts.days = "-"
+			}
+		},
+
+		// 设置HSTS有效期
+		setHSTSMaxAge: function (maxAge) {
+			this.hsts.maxAge = maxAge
+			this.changeHSTSMaxAge()
+		},
+
+		// 添加HSTS域名
+		addHstsDomain: function () {
+			this.hstsDomainAdding = true
+			this.hstsDomainEditingIndex = -1
+			let that = this
+			setTimeout(function () {
+				that.$refs.addingHstsDomain.focus()
+			}, 100)
+		},
+
+		// 修改HSTS域名
+		editHstsDomain: function (index) {
+			this.hstsDomainEditingIndex = index
+			this.addingHstsDomain = this.hsts.domains[index]
+			this.hstsDomainAdding = true
+			let that = this
+			setTimeout(function () {
+				that.$refs.addingHstsDomain.focus()
+			}, 100)
+		},
+
+		// 确认HSTS域名添加
+		confirmAddHstsDomain: function () {
+			this.addingHstsDomain = this.addingHstsDomain.trim()
+			if (this.addingHstsDomain.length == 0) {
+				return;
+			}
+			if (this.hstsDomainEditingIndex > -1) {
+				this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain
+			} else {
+				this.hsts.domains.push(this.addingHstsDomain)
+			}
+			this.cancelHstsDomainAdding()
+		},
+
+		// 取消HSTS域名添加
+		cancelHstsDomainAdding: function () {
+			this.hstsDomainAdding = false
+			this.addingHstsDomain = ""
+			this.hstsDomainEditingIndex = -1
+		},
+
+		// 删除HSTS域名
+		removeHstsDomain: function (index) {
+			this.cancelHstsDomainAdding()
+			this.hsts.domains.$remove(index)
+		},
+
+		// 选择客户端CA证书
+		selectClientCACert: function () {
+			let that = this
+			teaweb.popup("/servers/certs/selectPopup?isCA=1", {
+				width: "50em",
+				height: "30em",
+				callback: function (resp) {
+					that.policy.clientCARefs.push(resp.data.certRef)
+					that.policy.clientCACerts.push(resp.data.cert)
+				}
+			})
+		},
+
+		// 上传CA证书
+		uploadClientCACert: function () {
+			let that = this
+			teaweb.popup("/servers/certs/uploadPopup?isCA=1", {
+				height: "28em",
+				callback: function (resp) {
+					teaweb.success("上传成功", function () {
+						that.policy.clientCARefs.push(resp.data.certRef)
+						that.policy.clientCACerts.push(resp.data.cert)
+					})
+				}
+			})
+		},
+
+		// 删除客户端CA证书
+		removeClientCACert: function (index) {
+			let that = this
+			teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
+				that.policy.clientCARefs.$remove(index)
+				that.policy.clientCACerts.$remove(index)
+			})
+		}
+	},
+	template: `
+	
SSL/TLS相关配置 
+	
+	
+		
+			
+				启用HTTP/2 
+				
+					
+						 
+						 
+					
+				 
+			 
+			
+				选择证书 
+				
+					
+						
+							{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   
+						
+						
+					
 
+					
+						
选择或上传证书后HTTPS TLS 服务才能生效。 
+						
+					
 
+					选择已有证书   
+					上传新证书   
+					申请免费证书 
+				 
+			 
+			
+				TLS最低版本 
+				
+					
+						{{version}} 
+					 
+				 
+			 
+		 
+		 
+		
+			
+			
+				加密算法套件(CipherSuites)  
+				
+					
+						 
+						是否要自定义 
+					
+					
+						
+						
+							已添加套件({{policy.cipherSuites.length}}):
+							
+						
+						
+		
+						
+						
+					
 
+				 
+			 
+			
+			
+			
+				是否开启HSTS 
+				
+					
+						 
+						 
+					
+					
+				 
+			 
+			
+				HSTS包含子域名(includeSubDomains)  
+				
+					
+						 
+						 
+					
+				 
+			 
+			
+				HSTS预加载(preload)  
+				
+					
+						 
+						 
+					
+				 
+			 
+			
+				HSTS生效的域名 
+				
+					
+					
+					
+						+ 
+					
+					
+				 
+			 
+			
+			
+			
+				客户端认证方式 
+				
+					
+						{{authType.name}} 
+					 
+				 
+			 
+			
+				客户端认证CA证书 
+				
+					
+						
+							{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   
+						
+						
+					
 
+					选择已有证书   
+					上传新证书 
+					
+				 
+			 
+		 	
+	
+	
+
 `
+})
+
+// Action列表
+Vue.component("http-firewall-actions-view", {
+	props: ["v-actions"],
+	template: `
+		
+			{{action.name}} ({{action.code.toUpperCase()}}) 
+		
             
+
 `
+})
+
+// 显示WAF规则的标签
+Vue.component("http-firewall-rule-label", {
+	props: ["v-rule"],
+	data: function () {
+		return {
+			rule: this.vRule
+		}
+	},
+	methods: {
+		showErr: function (err) {
+
+			teaweb.popupTip("规则校验错误,请修正:"  + teaweb.encodeHTML(err) + " ")
+		},
+
+	},
+	template: `
+	
+		{{rule.name}}[{{rule.param}}] 
+
+		
+		
+			{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
+		 
+
+		
+		
+			{{rule.checkpointOptions.allowDomains}}
+		 
+
+		
+			 | {{paramFilter.code}}  
+		{{rule.operator}}  
+		{{rule.value}}
+		 
+		
+		
规则错误 
+	
+
 `
+})
+
+// 缓存条件列表
+Vue.component("http-cache-refs-box", {
+	props: ["v-cache-refs"],
+	data: function () {
+		let refs = this.vCacheRefs
+		if (refs == null) {
+			refs = []
+		}
+		return {
+			refs: refs
+		}
+	},
+	methods: {
+		timeUnitName: function (unit) {
+			switch (unit) {
+				case "ms":
+					return "毫秒"
+				case "second":
+					return "秒"
+				case "minute":
+					return "分钟"
+				case "hour":
+					return "小时"
+				case "day":
+					return "天"
+				case "week":
+					return "周 "
+			}
+			return unit
+		}
+	},
+	template: `
+	
+	
+	
+	
+		
+			
+				
+					缓存条件 
+					分组关系 
+					缓存时间 
+				 
+				
+					
+						 
+						
+							{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
+							- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} 
+						 
+						0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} 
+						状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} 
+					 
+					
+						和 
+						或 
+					 
+					
+						{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} 
+						不缓存 
+					 
+				 
+			 
+		
+	
+	
+
 `
+})
+
+Vue.component("ssl-certs-box", {
+	props: [
+		"v-certs", // 证书列表
+		"v-protocol", // 协议:https|tls
+		"v-view-size", // 弹窗尺寸
+		"v-single-mode" // 单证书模式
+	],
+	data: function () {
+		let certs = this.vCerts
+		if (certs == null) {
+			certs = []
+		}
+
+		return {
+			certs: certs
+		}
+	},
+	methods: {
+		certIds: function () {
+			return this.certs.map(function (v) {
+				return v.id
+			})
+		},
+		// 删除证书
+		removeCert: function (index) {
+			let that = this
+			teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
+				that.certs.$remove(index)
+			})
+		},
+
+		// 选择证书
+		selectCert: function () {
+			let that = this
+			let width = "50em"
+			let height = "30em"
+			let viewSize = this.vViewSize
+			if (viewSize == null) {
+				viewSize = "normal"
+			}
+			if (viewSize == "mini") {
+				width = "35em"
+				height = "20em"
+			}
+			teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize, {
+				width: width,
+				height: height,
+				callback: function (resp) {
+					that.certs.push(resp.data.cert)
+				}
+			})
+		},
+
+		// 上传证书
+		uploadCert: function () {
+			let that = this
+			teaweb.popup("/servers/certs/uploadPopup", {
+				height: "28em",
+				callback: function (resp) {
+					teaweb.success("上传成功", function () {
+						that.certs.push(resp.data.cert)
+					})
+				}
+			})
+		},
+
+		// 格式化时间
+		formatTime: function (timestamp) {
+			return new Date(timestamp * 1000).format("Y-m-d")
+		},
+
+		// 判断是否显示选择|上传按钮
+		buttonsVisible: function () {
+			return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0
+		}
+	},
+	template: `
+	
+	
+		
+			{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   
+		
+		
+	
+	
+		
选择或上传证书后HTTPS TLS 服务才能生效。 
+		
+	
+	
+		选择已有证书   
+		上传新证书   
+	
+
 `
+})
+
+Vue.component("http-host-redirect-box", {
+	props: ["v-redirects"],
+	mounted: function () {
+		let that = this
+		sortTable(function (ids) {
+			let newRedirects = []
+			ids.forEach(function (id) {
+				that.redirects.forEach(function (redirect) {
+					if (redirect.id == id) {
+						newRedirects.push(redirect)
+					}
+				})
+			})
+			that.updateRedirects(newRedirects)
+		})
+	},
+	data: function () {
+		let redirects = this.vRedirects
+		if (redirects == null) {
+			redirects = []
+		}
+
+		let id = 0
+		redirects.forEach(function (v) {
+			id++
+			v.id = id
+		})
+
+		return {
+			redirects: redirects,
+			statusOptions: [
+				{"code": 301, "text": "Moved Permanently"},
+				{"code": 308, "text": "Permanent Redirect"},
+				{"code": 302, "text": "Found"},
+				{"code": 303, "text": "See Other"},
+				{"code": 307, "text": "Temporary Redirect"}
+			],
+			id: id
+		}
+	},
+	methods: {
+		add: function () {
+			let that = this
+			window.UPDATING_REDIRECT = null
+
+			teaweb.popup("/servers/server/settings/redirects/createPopup", {
+				width: "50em",
+				height: "30em",
+				callback: function (resp) {
+					that.id++
+					resp.data.redirect.id = that.id
+					that.redirects.push(resp.data.redirect)
+					that.change()
+				}
+			})
+		},
+		update: function (index, redirect) {
+			let that = this
+			window.UPDATING_REDIRECT = redirect
+
+			teaweb.popup("/servers/server/settings/redirects/createPopup", {
+				width: "50em",
+				height: "30em",
+				callback: function (resp) {
+					resp.data.redirect.id = redirect.id
+					Vue.set(that.redirects, index, resp.data.redirect)
+					that.change()
+				}
+			})
+		},
+		remove: function (index) {
+			let that = this
+			teaweb.confirm("确定要删除这条跳转规则吗?", function () {
+				that.redirects.$remove(index)
+				that.change()
+			})
+		},
+		change: function () {
+			let that = this
+			setTimeout(function (){
+				that.$emit("change", that.redirects)
+			}, 100)
+		},
+		updateRedirects: function (newRedirects) {
+			this.redirects = newRedirects
+			this.change()
+		}
+	},
+	template: `
+	
+	
+	
+		[创建] 
+	 
+	
+
+	
+	
+		
+			
+				
+					 
+					跳转前URL 
+					 
+					跳转后URL 
+					匹配模式 
+					HTTP状态码 
+					状态 
+					操作 
+				 
+			 
+			
+				
+					   
+					
+						{{redirect.beforeURL}}
+						
+							匹配条件 
+						
+					 
+					-> 
+					{{redirect.afterURL}} 
+					
+						匹配前缀 
+						正则匹配 
+						精准匹配 
+					 
+					
+						{{redirect.status}} 
+						默认 
+					 
+					 
+					
+						修改   
+						删除 	
+					 
+				 
+			 
+		
+		
+	
+	
+
 `
+})
+
+// 单个缓存条件设置
+Vue.component("http-cache-ref-box", {
+	props: ["v-cache-ref", "v-is-reverse"],
+	data: function () {
+		let ref = this.vCacheRef
+		if (ref == null) {
+			ref = {
+				isOn: true,
+				cachePolicyId: 0,
+				key: "${scheme}://${host}${requestURI}",
+				life: {count: 2, unit: "hour"},
+				status: [200],
+				maxSize: {count: 32, unit: "mb"},
+				minSize: {count: 0, unit: "kb"},
+				skipCacheControlValues: ["private", "no-cache", "no-store"],
+				skipSetCookie: true,
+				enableRequestCachePragma: false,
+				conds: null,
+				allowChunkedEncoding: true,
+				isReverse: this.vIsReverse
+			}
+		}
+		if (ref.life == null) {
+			ref.life = {count: 2, unit: "hour"}
+		}
+		if (ref.maxSize == null) {
+			ref.maxSize = {count: 32, unit: "mb"}
+		}
+		if (ref.minSize == null) {
+			ref.minSize = {count: 0, unit: "kb"}
+		}
+		return {
+			ref: ref,
+			moreOptionsVisible: false
+		}
+	},
+	methods: {
+		changeOptionsVisible: function (v) {
+			this.moreOptionsVisible = v
+		},
+		changeLife: function (v) {
+			this.ref.life = v
+		},
+		changeMaxSize: function (v) {
+			this.ref.maxSize = v
+		},
+		changeMinSize: function (v) {
+			this.ref.minSize = v
+		},
+		changeConds: function (v) {
+			this.ref.conds = v
+		},
+		changeStatusList: function (list) {
+			let result = []
+			list.forEach(function (status) {
+				let statusNumber = parseInt(status)
+				if (isNaN(statusNumber) || statusNumber < 100 || statusNumber > 999) {
+					return
+				}
+				result.push(statusNumber)
+			})
+			this.ref.status = result
+		}
+	},
+	template: `
+	
+		匹配条件分组 * 
+		
+			 
+			
+			 
+		 
+	 
+	
+		缓存有效期 * 
+		
+			 
+		 
+	 
+	
+		缓存Key * 
+		
+			 
+			
+		 
+	 
+	
+		 
+	 
+	
+		可缓存的最大内容尺寸 
+		
+			 
+			
+		 
+	 
+	
+		可缓存的最小内容尺寸 
+		
+			 
+			
+		 
+	 
+	
+		支持分片内容 
+		
+			 
+			
+		 
+	 
+	
+		状态码列表 
+		
+			 
+			
+		 
+	 
+	
+		跳过的Cache-Control值 
+		
+			 
+			
+		 
+	 
+	
+		跳过Set-Cookie 
+		
+			
+				 
+				 
+			
+			
+		 
+	 
+	
+		支持请求no-cache刷新 
+		
+			
+				 
+				 
+			
+			
+		 
+	 	
+ `
+})
+
+// 浏览条件列表
+Vue.component("http-request-conds-view", {
+	props: ["v-conds"],
+	data: function () {
+		let conds = this.vConds
+		if (conds == null) {
+			conds = {
+				isOn: true,
+				connector: "or",
+				groups: []
+			}
+		}
+
+		let that = this
+		conds.groups.forEach(function (group) {
+			group.conds.forEach(function (cond) {
+				cond.typeName = that.typeName(cond)
+			})
+		})
+
+		return {
+			initConds: conds,
+			version: 0 // 为了让组件能及时更新加入此变量
+		}
+	},
+	computed: {
+		// 之所以使用computed,是因为需要动态更新
+		conds: function () {
+			return this.initConds
+		}
+	},
+	methods: {
+		typeName: function (cond) {
+			let c = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
+				return v.type == cond.type
+			})
+			if (c != null) {
+				return c.name;
+			}
+			return cond.param + " " + cond.operator
+		},
+		notifyChange: function () {
+			this.version++
+			let that = this
+			this.initConds.groups.forEach(function (group) {
+				group.conds.forEach(function (cond) {
+					cond.typeName = that.typeName(cond)
+				})
+			})
+		}
+	},
+	template: `
+		
{{version}} 
+		
+			
+				
+					
+						{{cond.param}} {{cond.operator}}  
+						{{cond.typeName}}:  
+						{{cond.value}}
+					 
+					
+					 {{group.connector}}   
+				 
+				
+				
+					{{group.description}} 
+				
+			
	
+		
+	
 	
+`
+})
+
+Vue.component("http-firewall-config-box", {
+	props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"],
+	data: function () {
+		let firewall = this.vFirewallConfig
+		if (firewall == null) {
+			firewall = {
+				isPrior: false,
+				isOn: false,
+				firewallPolicyId: 0
+			}
+		}
+
+		return {
+			firewall: firewall
+		}
+	},
+	template: ``
+})
+
+// 指标图表
+Vue.component("metric-chart", {
+	props: ["v-chart", "v-stats", "v-item"],
+	mounted: function () {
+		this.load()
+	},
+	data: function () {
+		let stats = this.vStats
+		if (stats == null) {
+			stats = []
+		}
+		if (stats.length > 0) {
+			let sum = stats.$sum(function (k, v) {
+				return v.value
+			})
+			if (sum < stats[0].total) {
+				if (this.vChart.type == "pie") {
+					stats.push({
+						keys: ["其他"],
+						value: stats[0].total - sum,
+						total: stats[0].total,
+						time: stats[0].time
+					})
+				}
+			}
+		}
+		if (this.vChart.maxItems > 0) {
+			stats = stats.slice(0, this.vChart.maxItems)
+		} else {
+			stats = stats.slice(0, 10)
+		}
+
+		stats.$rsort(function (v1, v2) {
+			return v1.value - v2.value
+		})
+
+		let widthPercent = 100
+		if (this.vChart.widthDiv > 0) {
+			widthPercent = 100 / this.vChart.widthDiv
+		}
+
+		return {
+			chart: this.vChart,
+			stats: stats,
+			item: this.vItem,
+			width: widthPercent + "%",
+			chartId: "metric-chart-" + this.vChart.id,
+			valueTypeName: (this.vItem != null && this.vItem.valueTypeName != null && this.vItem.valueTypeName.length > 0) ? this.vItem.valueTypeName : ""
+		}
+	},
+	methods: {
+		load: function () {
+			var el = document.getElementById(this.chartId)
+			if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) {
+				setTimeout(this.load, 100)
+			} else {
+				this.render(el)
+			}
+		},
+		render: function (el) {
+			let chart = echarts.init(el)
+			window.addEventListener("resize", function () {
+				chart.resize()
+			})
+			switch (this.chart.type) {
+				case "pie":
+					this.renderPie(chart)
+					break
+				case "bar":
+					this.renderBar(chart)
+					break
+				case "timeBar":
+					this.renderTimeBar(chart)
+					break
+				case "timeLine":
+					this.renderTimeLine(chart)
+					break
+				case "table":
+					this.renderTable(chart)
+					break
+			}
+		},
+		renderPie: function (chart) {
+			let values = this.stats.map(function (v) {
+				return {
+					name: v.keys[0],
+					value: v.value
+				}
+			})
+			let that = this
+			chart.setOption({
+				tooltip: {
+					show: true,
+					trigger: "item",
+					formatter: function (data) {
+						let stat = that.stats[data.dataIndex]
+						let percent = 0
+						if (stat.total > 0) {
+							percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
+						}
+						let value = stat.value
+						switch (that.item.valueType) {
+							case "byte":
+								value = teaweb.formatBytes(value)
+								break
+						}
+						return stat.keys[0] + ": " + value + ",占比:" + percent + "%"
+					}
+				},
+				series: [
+					{
+						name: name,
+						type: "pie",
+						data: values,
+						areaStyle: {},
+						color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"]
+					}
+				]
+			})
+		},
+		renderTimeBar: function (chart) {
+			this.stats.$sort(function (v1, v2) {
+				return (v1.time < v2.time) ? -1 : 1
+			})
+			let values = this.stats.map(function (v) {
+				return v.value
+			})
+
+			let axis = {unit: "", divider: 1}
+			switch (this.item.valueType) {
+				case "count":
+					axis = teaweb.countAxis(values, function (v) {
+						return v
+					})
+					break
+				case "byte":
+					axis = teaweb.bytesAxis(values, function (v) {
+						return v
+					})
+					break
+			}
+
+			let that = this
+			chart.setOption({
+				xAxis: {
+					data: this.stats.map(function (v) {
+						return that.formatTime(v.time)
+					})
+				},
+				yAxis: {
+					axisLabel: {
+						formatter: function (value) {
+							return value + axis.unit
+						}
+					}
+				},
+				tooltip: {
+					show: true,
+					trigger: "item",
+					formatter: function (data) {
+						let stat = that.stats[data.dataIndex]
+						let value = stat.value
+						switch (that.item.valueType) {
+							case "byte":
+								value = teaweb.formatBytes(value)
+								break
+						}
+						return that.formatTime(stat.time) + ": " + value
+					}
+				},
+				grid: {
+					left: 50,
+					top: 10,
+					right: 20,
+					bottom: 25
+				},
+				series: [
+					{
+						name: name,
+						type: "bar",
+						data: values.map(function (v) {
+							return v / axis.divider
+						}),
+						itemStyle: {
+							color: "#9DD3E8"
+						},
+						areaStyle: {},
+						barWidth: "20em"
+					}
+				]
+			})
+		},
+		renderTimeLine: function (chart) {
+			this.stats.$sort(function (v1, v2) {
+				return (v1.time < v2.time) ? -1 : 1
+			})
+			let values = this.stats.map(function (v) {
+				return v.value
+			})
+
+			let axis = {unit: "", divider: 1}
+			switch (this.item.valueType) {
+				case "count":
+					axis = teaweb.countAxis(values, function (v) {
+						return v
+					})
+					break
+				case "byte":
+					axis = teaweb.bytesAxis(values, function (v) {
+						return v
+					})
+					break
+			}
+
+			let that = this
+			chart.setOption({
+				xAxis: {
+					data: this.stats.map(function (v) {
+						return that.formatTime(v.time)
+					})
+				},
+				yAxis: {
+					axisLabel: {
+						formatter: function (value) {
+							return value + axis.unit
+						}
+					}
+				},
+				tooltip: {
+					show: true,
+					trigger: "item",
+					formatter: function (data) {
+						let stat = that.stats[data.dataIndex]
+						let value = stat.value
+						switch (that.item.valueType) {
+							case "byte":
+								value = teaweb.formatBytes(value)
+								break
+						}
+						return that.formatTime(stat.time) + ": " + value
+					}
+				},
+				grid: {
+					left: 50,
+					top: 10,
+					right: 20,
+					bottom: 25
+				},
+				series: [
+					{
+						name: name,
+						type: "line",
+						data: values.map(function (v) {
+							return v / axis.divider
+						}),
+						itemStyle: {
+							color: "#9DD3E8"
+						},
+						areaStyle: {}
+					}
+				]
+			})
+		},
+		renderBar: function (chart) {
+			let values = this.stats.map(function (v) {
+				return v.value
+			})
+			let axis = {unit: "", divider: 1}
+			switch (this.item.valueType) {
+				case "count":
+					axis = teaweb.countAxis(values, function (v) {
+						return v
+					})
+					break
+				case "byte":
+					axis = teaweb.bytesAxis(values, function (v) {
+						return v
+					})
+					break
+			}
+			let bottom = 24
+			let rotate = 0
+			let result = teaweb.xRotation(chart, this.stats.map(function (v) {
+				return v.keys[0]
+			}))
+			if (result != null) {
+				bottom = result[0]
+				rotate = result[1]
+			}
+			let that = this
+			chart.setOption({
+				xAxis: {
+					data: this.stats.map(function (v) {
+						return v.keys[0]
+					}),
+					axisLabel: {
+						interval: 0,
+						rotate: rotate
+					}
+				},
+				tooltip: {
+					show: true,
+					trigger: "item",
+					formatter: function (data) {
+						let stat = that.stats[data.dataIndex]
+						let percent = 0
+						if (stat.total > 0) {
+							percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
+						}
+						let value = stat.value
+						switch (that.item.valueType) {
+							case "byte":
+								value = teaweb.formatBytes(value)
+								break
+						}
+						return stat.keys[0] + ": " + value + ",占比:" + percent + "%"
+					}
+				},
+				yAxis: {
+					axisLabel: {
+						formatter: function (value) {
+							return value + axis.unit
+						}
+					}
+				},
+				grid: {
+					left: 40,
+					top: 10,
+					right: 20,
+					bottom: bottom
+				},
+				series: [
+					{
+						name: name,
+						type: "bar",
+						data: values.map(function (v) {
+							return v / axis.divider
+						}),
+						itemStyle: {
+							color: "#9DD3E8"
+						},
+						areaStyle: {},
+						barWidth: "20em"
+					}
+				]
+			})
+
+			if (this.item.keys != null) {
+				// IP相关操作
+				if (this.item.keys.$contains("${remoteAddr}")) {
+					let that = this
+					chart.on("click", function (args) {
+						let index = that.item.keys.$indexesOf("${remoteAddr}")[0]
+						let value = that.stats[args.dataIndex].keys[index]
+						teaweb.popup("/servers/ipbox?ip=" + value, {
+							width: "50em",
+							height: "30em"
+						})
+					})
+				}
+			}
+		},
+		renderTable: function (chart) {
+			let table = `
+	
+		
+			对象 
+			数值 
+			占比 
+		 
+	 `
+			let that = this
+			this.stats.forEach(function (v) {
+				let value = v.value
+				switch (that.item.valueType) {
+					case "byte":
+						value = teaweb.formatBytes(value)
+						break
+				}
+				table += "" + v.keys[0] + " " + value + " "
+				let percent = 0
+				if (v.total > 0) {
+					percent = Math.round((v.value * 100 / v.total) * 100) / 100
+				}
+				table += "" + percent + "% "
+				table += " "
+			})
+
+			table += `
`
+			document.getElementById(this.chartId).innerHTML = table
+		},
+		formatTime: function (time) {
+			if (time == null) {
+				return ""
+			}
+			switch (this.item.periodUnit) {
+				case "month":
+					return time.substring(0, 4) + "-" + time.substring(4, 6)
+				case "week":
+					return time.substring(0, 4) + "-" + time.substring(4, 6)
+				case "day":
+					return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8)
+				case "hour":
+					return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10)
+				case "minute":
+					return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10) + ":" + time.substring(10, 12)
+			}
+			return time
+		}
+	},
+	template: `
+	
{{chart.name}} ({{valueTypeName}})  
+	
+	
+
 `
+})
+
+Vue.component("metric-board", {
+	template: `
`
+})
+
+Vue.component("http-cache-config-box", {
+	props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy"],
+	data: function () {
+		let cacheConfig = this.vCacheConfig
+		if (cacheConfig == null) {
+			cacheConfig = {
+				isPrior: false,
+				isOn: false,
+				addStatusHeader: true,
+				cacheRefs: [],
+				purgeIsOn: false,
+				purgeKey: ""
+			}
+		}
+		if (cacheConfig.cacheRefs == null) {
+			cacheConfig.cacheRefs = []
+		}
+		return {
+			cacheConfig: cacheConfig,
+			moreOptionsVisible: false
+		}
+	},
+	methods: {
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
+		},
+		generatePurgeKey: function () {
+			let r = Math.random().toString() + Math.random().toString()
+			let s = r.replace(/0\./g, "")
+				.replace(/\./g, "")
+			let result = ""
+			for (let i = 0; i < s.length; i++) {
+				result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0))
+			}
+			this.cacheConfig.purgeKey = result
+		},
+		showMoreOptions: function () {
+			this.moreOptionsVisible = !this.moreOptionsVisible
+		}
+	},
+	template: ``
+})
+
+// 通用Header长度
+let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"]
+Vue.component("http-cond-general-header-length", {
+	props: ["v-checkpoint"],
+	data: function () {
+		let headers = null
+		let length = null
+
+		if (window.parent.UPDATING_RULE != null) {
+			let options = window.parent.UPDATING_RULE.checkpointOptions
+			if (options.headers != null && Array.$isArray(options.headers)) {
+				headers = options.headers
+			}
+			if (options.length != null) {
+				length = options.length
+			}
+		}
+
+
+		if (headers == null) {
+			headers = defaultGeneralHeaders
+		}
+
+		if (length == null) {
+			length = 128
+		}
+
+		let that = this
+		setTimeout(function () {
+			that.change()
+		}, 100)
+
+		return {
+			headers: headers,
+			length: length
+		}
+	},
+	watch: {
+		length: function (v) {
+			let len = parseInt(v)
+			if (isNaN(len)) {
+				len = 0
+			}
+			if (len < 0) {
+				len = 0
+			}
+			this.length = len
+			this.change()
+		}
+	},
+	methods: {
+		change: function () {
+			this.vCheckpoint.options = [
+				{
+					code: "headers",
+					value: this.headers
+				},
+				{
+					code: "length",
+					value: this.length
+				}
+			]
+		}
+	},
+	template: ``
+})
+
+// CC
+Vue.component("http-firewall-checkpoint-cc", {
+	props: ["v-checkpoint"],
+	data: function () {
+		let keys = []
+		let period = 60
+		let threshold = 1000
+
+		let options = {}
+		if (window.parent.UPDATING_RULE != null) {
+			options = window.parent.UPDATING_RULE.checkpointOptions
+		}
+
+		if (options == null) {
+			options = {}
+		}
+		if (options.keys != null) {
+			keys = options.keys
+		}
+		if (keys.length == 0) {
+			keys = ["${remoteAddr}", "${requestPath}"]
+		}
+		if (options.period != null) {
+			period = options.period
+		}
+		if (options.threshold != null) {
+			threshold = options.threshold
+		}
+
+		let that = this
+		setTimeout(function () {
+			that.change()
+		}, 100)
+
+		return {
+			keys: keys,
+			period: period,
+			threshold: threshold,
+			options: {},
+			value: threshold
+		}
+	},
+	watch: {
+		period: function () {
+			this.change()
+		},
+		threshold: function () {
+			this.change()
+		}
+	},
+	methods: {
+		changeKeys: function (keys) {
+			this.keys = keys
+			this.change()
+		},
+		change: function () {
+			let period = parseInt(this.period.toString())
+			if (isNaN(period) || period <= 0) {
+				period = 60
+			}
+
+			let threshold = parseInt(this.threshold.toString())
+			if (isNaN(threshold) || threshold <= 0) {
+				threshold = 1000
+			}
+			this.value = threshold
+
+			this.vCheckpoint.options = [
+				{
+					code: "keys",
+					value: this.keys
+				},
+				{
+					code: "period",
+					value: period,
+				},
+				{
+					code: "threshold",
+					value: threshold
+				}
+			]
+		}
+	},
+	template: ``
+})
+
+// 防盗链
+Vue.component("http-firewall-checkpoint-referer-block", {
+	props: ["v-checkpoint"],
+	data: function () {
+		let allowEmpty = true
+		let allowSameDomain = true
+		let allowDomains = []
+
+		let options = {}
+		if (window.parent.UPDATING_RULE != null) {
+			options = window.parent.UPDATING_RULE.checkpointOptions
+		}
+
+		if (options == null) {
+			options = {}
+		}
+		if (typeof (options.allowEmpty) == "boolean") {
+			allowEmpty = options.allowEmpty
+		}
+		if (typeof (options.allowSameDomain) == "boolean") {
+			allowSameDomain = options.allowSameDomain
+		}
+		if (options.allowDomains != null && typeof (options.allowDomains) == "object") {
+			allowDomains = options.allowDomains
+		}
+
+		let that = this
+		setTimeout(function () {
+			that.change()
+		}, 100)
+
+		return {
+			allowEmpty: allowEmpty,
+			allowSameDomain: allowSameDomain,
+			allowDomains: allowDomains,
+			options: {},
+			value: 0
+		}
+	},
+	watch: {
+		allowEmpty: function () {
+			this.change()
+		},
+		allowSameDomain: function () {
+			this.change()
+		}
+	},
+	methods: {
+		changeAllowDomains: function (values) {
+			this.allowDomains = values
+			this.change()
+		},
+		change: function () {
+			this.vCheckpoint.options = [
+				{
+					code: "allowEmpty",
+					value: this.allowEmpty
+				},
+				{
+					code: "allowSameDomain",
+					value: this.allowSameDomain,
+				},
+				{
+					code: "allowDomains",
+					value: this.allowDomains
+				}
+			]
+		}
+	},
+	template: `
+	
+	
+	
+		
+			来源域名允许为空 
+			
+				 
+				
+			 
+		 
+		
+			来源域名允许一致 
+			
+				 
+				
+			 
+		 
+		
+			允许的来源域名 
+			
+				 
+				
+			 
+		 
+	
+
 `
+})
+
+Vue.component("http-cache-refs-config-box", {
+	props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id"],
+	mounted: function () {
+		let that = this
+		sortTable(function (ids) {
+			let newRefs = []
+			ids.forEach(function (id) {
+				that.refs.forEach(function (ref) {
+					if (ref.id == id) {
+						newRefs.push(ref)
+					}
+				})
+			})
+			that.updateRefs(newRefs)
+			that.change()
+		})
+	},
+	data: function () {
+		let refs = this.vCacheRefs
+		if (refs == null) {
+			refs = []
+		}
+
+		let id = 0
+		refs.forEach(function (ref) {
+			id++
+			ref.id = id
+		})
+		return {
+			refs: refs,
+			id: id // 用来对条件进行排序
+		}
+	},
+	methods: {
+		addRef: function (isReverse) {
+			window.UPDATING_CACHE_REF = null
+
+			let width = window.innerWidth
+			if (width > 1024) {
+				width = 1024
+			}
+			let height = window.innerHeight
+			if (height > 500) {
+				height = 500
+			}
+			let that = this
+			teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), {
+				width: width + "px",
+				height: height + "px",
+				callback: function (resp) {
+					let newRef = resp.data.cacheRef
+					if (newRef.conds == null) {
+						return
+					}
+
+					that.id++
+					newRef.id = that.id
+
+					if (newRef.isReverse) {
+						let newRefs = []
+						let isAdded = false
+						that.refs.forEach(function (v) {
+							if (!v.isReverse && !isAdded) {
+								newRefs.push(newRef)
+								isAdded = true
+							}
+							newRefs.push(v)
+						})
+						if (!isAdded) {
+							newRefs.push(newRef)
+						}
+
+						that.updateRefs(newRefs)
+					} else {
+						that.refs.push(newRef)
+					}
+
+					that.change()
+				}
+			})
+		},
+		updateRef: function (index, cacheRef) {
+			window.UPDATING_CACHE_REF = cacheRef
+
+			let width = window.innerWidth
+			if (width > 1024) {
+				width = 1024
+			}
+			let height = window.innerHeight
+			if (height > 500) {
+				height = 500
+			}
+			let that = this
+			teaweb.popup("/servers/server/settings/cache/createPopup", {
+				width: width + "px",
+				height: height + "px",
+				callback: function (resp) {
+					resp.data.cacheRef.id = that.refs[index].id
+					Vue.set(that.refs, index, resp.data.cacheRef)
+
+					// 通知子组件更新
+					that.$refs.cacheRef[index].notifyChange()
+
+					that.change()
+				}
+			})
+		},
+		removeRef: function (index) {
+			let that = this
+			teaweb.confirm("确定要删除此缓存设置吗?", function () {
+				that.refs.$remove(index)
+				that.change()
+			})
+		},
+		updateRefs: function (newRefs) {
+			this.refs = newRefs
+			if (this.vCacheConfig != null) {
+				this.vCacheConfig.cacheRefs = newRefs
+			}
+		},
+		timeUnitName: function (unit) {
+			switch (unit) {
+				case "ms":
+					return "毫秒"
+				case "second":
+					return "秒"
+				case "minute":
+					return "分钟"
+				case "hour":
+					return "小时"
+				case "day":
+					return "天"
+				case "week":
+					return "周 "
+			}
+			return unit
+		},
+		change: function () {
+			// 自动保存
+			if (this.vCachePolicyId != null && this.vCachePolicyId > 0) {
+				Tea.action("/servers/components/cache/updateRefs")
+					.params({
+						cachePolicyId: this.vCachePolicyId,
+						refsJSON: JSON.stringify(this.refs)
+					})
+					.post()
+			}
+		}
+	},
+	template: `
+	
+	
+	
+		
+		
+			
+				
+					 
+					缓存条件 
+					分组关系 
+					缓存时间 
+					操作 
+				 
+			 	
+			
+				
+					   
+					
+						 
+						
+							{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
+							- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} 
+						 
+						0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} 
+						状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} 
+					 
+					
+						和 
+						或 
+					 
+					
+						{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} 
+						不缓存 
+					 
+					
+						修改   
+						删除 
+					 
+				 
+			 
+		
+		
+		
+		
+添加缓存设置      +添加不缓存设置 
+	
+	
+
 `
+})
+
+Vue.component("origin-list-box", {
+	props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"],
+	data: function () {
+		return {
+			primaryOrigins: this.vPrimaryOrigins,
+			backupOrigins: this.vBackupOrigins
+		}
+	},
+	methods: {
+		createPrimaryOrigin: function () {
+			teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, {
+				height: "27em",
+				callback: function (resp) {
+					teaweb.success("保存成功", function () {
+						window.location.reload()
+					})
+				}
+			})
+		},
+		createBackupOrigin: function () {
+			teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, {
+				height: "27em",
+				callback: function (resp) {
+					teaweb.success("保存成功", function () {
+						window.location.reload()
+					})
+				}
+			})
+		},
+		updateOrigin: function (originId, originType) {
+			teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, {
+				height: "27em",
+				callback: function (resp) {
+					teaweb.success("保存成功", function () {
+						window.location.reload()
+					})
+				}
+			})
+		},
+		deleteOrigin: function (originId, originType) {
+			let that = this
+			teaweb.confirm("确定要删除此源站吗?", function () {
+				Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType)
+					.post()
+					.success(function () {
+						teaweb.success("保存成功", function () {
+							window.location.reload()
+						})
+					})
+			})
+		}
+	},
+	template: ``
+})
+
+Vue.component("origin-list-table", {
+	props: ["v-origins", "v-origin-type"],
+	data: function () {
+		return {}
+	},
+	methods: {
+		deleteOrigin: function (originId) {
+			this.$emit("deleteOrigin", originId, this.vOriginType)
+		},
+		updateOrigin: function (originId) {
+			this.$emit("updateOrigin", originId, this.vOriginType)
+		}
+	},
+	template: `
+
+	
+		
+			源站地址 
+			权重 
+			状态 
+			操作 
+		 	
+	 
+	
+		{{origin.addr}}   
+			
+				{{origin.name}} 
+			
+			
+				{{domain}} 
+			
+		 
+		{{origin.weight}} 
+		
+			 
+		 
+		
+			修改   
+			删除 
+		 
+	 
+
`
+})
+
+Vue.component("http-firewall-policy-selector", {
+	props: ["v-http-firewall-policy"],
+	mounted: function () {
+		let that = this
+		Tea.action("/servers/components/waf/count")
+			.post()
+			.success(function (resp) {
+				that.count = resp.data.count
+			})
+	},
+	data: function () {
+		let firewallPolicy = this.vHttpFirewallPolicy
+		return {
+			count: 0,
+			firewallPolicy: firewallPolicy
+		}
+	},
+	methods: {
+		remove: function () {
+			this.firewallPolicy = null
+		},
+		select: function () {
+			let that = this
+			teaweb.popup("/servers/components/waf/selectPopup", {
+				callback: function (resp) {
+					that.firewallPolicy = resp.data.firewallPolicy
+				}
+			})
+		},
+		create: function () {
+			let that = this
+			teaweb.popup("/servers/components/waf/createPopup", {
+				height: "26em",
+				callback: function (resp) {
+					that.firewallPolicy = resp.data.firewallPolicy
+				}
+			})
+		}
+	},
+	template: `
+	
+		
+		{{firewallPolicy.name}}   
   
+	
+	
+
 `
+})
+
+Vue.component("http-websocket-box", {
+	props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let websocketRef = this.vWebsocketRef
+		if (websocketRef == null) {
+			websocketRef = {
+				isPrior: false,
+				isOn: false,
+				websocketId: 0
+			}
+		}
+
+		let websocketConfig = this.vWebsocketConfig
+		if (websocketConfig == null) {
+			websocketConfig = {
+				id: 0,
+				isOn: false,
+				handshakeTimeout: {
+					count: 30,
+					unit: "second"
+				},
+				allowAllOrigins: true,
+				allowedOrigins: [],
+				requestSameOrigin: true,
+				requestOrigin: ""
+			}
+		} else {
+			if (websocketConfig.handshakeTimeout == null) {
+				websocketConfig.handshakeTimeout = {
+					count: 30,
+					unit: "second",
+				}
+			}
+			if (websocketConfig.allowedOrigins == null) {
+				websocketConfig.allowedOrigins = []
+			}
+		}
+
+		return {
+			websocketRef: websocketRef,
+			websocketConfig: websocketConfig,
+			handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(),
+			advancedVisible: false
+		}
+	},
+	watch: {
+		handshakeTimeoutCountString: function (v) {
+			let count = parseInt(v)
+			if (!isNaN(count) && count >= 0) {
+				this.websocketConfig.handshakeTimeout.count = count
+			} else {
+				this.websocketConfig.handshakeTimeout.count = 0
+			}
+		}
+	},
+	methods: {
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn
+		},
+		changeAdvancedVisible: function (v) {
+			this.advancedVisible = v
+		},
+		createOrigin: function () {
+			let that = this
+			teaweb.popup("/servers/server/settings/websocket/createOrigin", {
+				height: "12.5em",
+				callback: function (resp) {
+					that.websocketConfig.allowedOrigins.push(resp.data.origin)
+				}
+			})
+		},
+		removeOrigin: function (index) {
+			this.websocketConfig.allowedOrigins.$remove(index)
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-rewrite-rule-list", {
+	props: ["v-web-id", "v-rewrite-rules"],
+	mounted: function () {
+		setTimeout(this.sort, 1000)
+	},
+	data: function () {
+		let rewriteRules = this.vRewriteRules
+		if (rewriteRules == null) {
+			rewriteRules = []
+		}
+		return {
+			rewriteRules: rewriteRules
+		}
+	},
+	methods: {
+		updateRewriteRule: function (rewriteRuleId) {
+			teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, {
+				height: "26em",
+				callback: function () {
+					window.location.reload()
+				}
+			})
+		},
+		deleteRewriteRule: function (rewriteRuleId) {
+			let that = this
+			teaweb.confirm("确定要删除此重写规则吗?", function () {
+				Tea.action("/servers/server/settings/rewrite/delete")
+					.params({
+						webId: that.vWebId,
+						rewriteRuleId: rewriteRuleId
+					})
+					.post()
+					.refresh()
+			})
+		},
+		// 排序
+		sort: function () {
+			if (this.rewriteRules.length == 0) {
+				return
+			}
+			let that = this
+			sortTable(function (rowIds) {
+				Tea.action("/servers/server/settings/rewrite/sort")
+					.post()
+					.params({
+						webId: that.vWebId,
+						rewriteRuleIds: rowIds
+					})
+					.success(function () {
+						teaweb.success("保存成功")
+					})
+			})
+		}
+	},
+	template: `
+	
+	
+	
+		
+			
+				 
+				匹配规则 
+				转发目标 
+				转发方式 
+				状态 
+				操作 
+			 
+		 
+		
+			
+				 
+				{{rule.pattern}}
+				 
+					BREAK 
+					{{rule.redirectStatus}} 
+					Host: {{rule.proxyHost}} 
+				 
+				{{rule.replace}} 
+				
+					隐式 
+					显示 
+				 
+				
+					 
+				 
+				
+					修改   
+					删除 
+				 
+			 
+		 
+	
+	
+
+
 `
+})
+
+Vue.component("http-rewrite-labels-label", {
+	props: ["v-class"],
+	template: ` `
+})
+
+Vue.component("server-name-box", {
+    props: ["v-server-names"],
+    data: function () {
+        let serverNames = this.vServerNames;
+        if (serverNames == null) {
+            serverNames = []
+        }
+        return {
+            serverNames: serverNames,
+            isSearching: false,
+            keyword: ""
+        }
+    },
+    methods: {
+        addServerName: function () {
+            window.UPDATING_SERVER_NAME = null
+            let that = this
+            teaweb.popup("/servers/addServerNamePopup", {
+                callback: function (resp) {
+                    var serverName = resp.data.serverName
+                    that.serverNames.push(serverName)
+                }
+            });
+        },
+
+        removeServerName: function (index) {
+            this.serverNames.$remove(index)
+        },
+
+        updateServerName: function (index, serverName) {
+            window.UPDATING_SERVER_NAME = serverName
+            let that = this
+            teaweb.popup("/servers/addServerNamePopup", {
+                callback: function (resp) {
+                    var serverName = resp.data.serverName
+                    Vue.set(that.serverNames, index, serverName)
+                }
+            });
+        },
+        showSearchBox: function () {
+            this.isSearching = !this.isSearching
+            if (this.isSearching) {
+                let that = this
+                setTimeout(function () {
+                    that.$refs.keywordRef.focus()
+                }, 200)
+            } else {
+                this.keyword = ""
+            }
+        },
+    },
+    watch: {
+        keyword: function (v) {
+            this.serverNames.forEach(function (serverName) {
+                if (v.length == 0) {
+                    serverName.isShowing = true
+                    return
+                }
+                if (serverName.subNames == null || serverName.subNames.length == 0) {
+                    if (!teaweb.match(serverName.name, v)) {
+                        serverName.isShowing = false
+                    }
+                } else {
+                    let found = false
+                    serverName.subNames.forEach(function (subName) {
+                        if (teaweb.match(subName, v)) {
+                            found = true
+                        }
+                    })
+                    serverName.isShowing = found
+                }
+            })
+        }
+    },
+    template: `
+	
+	
+		
+			
{{serverName.type}}   
+			
{{serverName.name}} 
+			
{{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 
+			
  
+		
+		
+	
+	
+
 `
+})
+
+// 域名列表
+Vue.component("domains-box", {
+	props: ["v-domains"],
+	data: function () {
+		let domains = this.vDomains
+		if (domains == null) {
+			domains = []
+		}
+		return {
+			domains: domains,
+			isAdding: false,
+			addingDomain: ""
+		}
+	},
+	methods: {
+		add: function () {
+			this.isAdding = true
+			let that = this
+			setTimeout(function () {
+				that.$refs.addingDomain.focus()
+			}, 100)
+		},
+		confirm: function () {
+			let that = this
+
+			// 删除其中的空格
+			this.addingDomain = this.addingDomain.replace(/\s/g, "")
+
+			if (this.addingDomain.length == 0) {
+				teaweb.warn("请输入要添加的域名", function () {
+					that.$refs.addingDomain.focus()
+				})
+				return
+			}
+
+
+			// 基本校验
+			if (this.addingDomain[0] == "~") {
+				let expr = this.addingDomain.substring(1)
+				try {
+					new RegExp(expr)
+				} catch (e) {
+					teaweb.warn("正则表达式错误:" + e.message, function () {
+						that.$refs.addingDomain.focus()
+					})
+					return
+				}
+			}
+
+			this.domains.push(this.addingDomain)
+			this.cancel()
+		},
+		remove: function (index) {
+			this.domains.$remove(index)
+		},
+		cancel: function () {
+			this.isAdding = false
+			this.addingDomain = ""
+		}
+	},
+	template: `
+	
+	
+		
+			[正则] 
+			[后缀] 
+			[泛域名] 
+			{{domain}}
+			   
+		 
+		
+	
+	
+	
+		+ 
+	
+
 `
+})
+
+Vue.component("http-redirect-to-https-box", {
+	props: ["v-redirect-to-https-config", "v-is-location"],
+	data: function () {
+		let redirectToHttpsConfig = this.vRedirectToHttpsConfig
+		if (redirectToHttpsConfig == null) {
+			redirectToHttpsConfig = {
+				isPrior: false,
+				isOn: false,
+				host: "",
+				port: 0,
+				status: 0,
+				onlyDomains: [],
+				exceptDomains: []
+			}
+		} else {
+			if (redirectToHttpsConfig.onlyDomains == null) {
+				redirectToHttpsConfig.onlyDomains = []
+			}
+			if (redirectToHttpsConfig.exceptDomains == null) {
+				redirectToHttpsConfig.exceptDomains = []
+			}
+		}
+		return {
+			redirectToHttpsConfig: redirectToHttpsConfig,
+			portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "",
+			moreOptionsVisible: false,
+			statusOptions: [
+				{"code": 301, "text": "Moved Permanently"},
+				{"code": 308, "text": "Permanent Redirect"},
+				{"code": 302, "text": "Found"},
+				{"code": 303, "text": "See Other"},
+				{"code": 307, "text": "Temporary Redirect"}
+			]
+		}
+	},
+	watch: {
+		"redirectToHttpsConfig.status": function () {
+			this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status)
+		},
+		portString: function (v) {
+			let port = parseInt(v)
+			if (!isNaN(port)) {
+				this.redirectToHttpsConfig.port = port
+			} else {
+				this.redirectToHttpsConfig.port = 0
+			}
+		}
+	},
+	methods: {
+		changeMoreOptions: function (isVisible) {
+			this.moreOptionsVisible = isVisible
+		},
+		changeOnlyDomains: function (values) {
+			this.redirectToHttpsConfig.onlyDomains = values
+			this.$forceUpdate()
+		},
+		changeExceptDomains: function (values) {
+			this.redirectToHttpsConfig.exceptDomains = values
+			this.$forceUpdate()
+		}
+	},
+	template: ``
+})
+
+// 动作选择
+Vue.component("http-firewall-actions-box", {
+	props: ["v-actions", "v-firewall-policy", "v-action-configs"],
+	mounted: function () {
+		let that = this
+		Tea.action("/servers/iplists/levelOptions")
+			.success(function (resp) {
+				that.ipListLevels = resp.data.levels
+			})
+			.post()
+
+		this.loadJS(function () {
+			let box = document.getElementById("actions-box")
+			Sortable.create(box, {
+				draggable: ".label",
+				handle: ".icon.handle",
+				onStart: function () {
+					that.cancel()
+				},
+				onUpdate: function (event) {
+					let labels = box.getElementsByClassName("label")
+					let newConfigs = []
+					for (let i = 0; i < labels.length; i++) {
+						let index = parseInt(labels[i].getAttribute("data-index"))
+						newConfigs.push(that.configs[index])
+					}
+					that.configs = newConfigs
+				}
+			})
+		})
+	},
+	data: function () {
+		if (this.vFirewallPolicy.inbound == null) {
+			this.vFirewallPolicy.inbound = {}
+		}
+		if (this.vFirewallPolicy.inbound.groups == null) {
+			this.vFirewallPolicy.inbound.groups = []
+		}
+
+		let id = 0
+		let configs = []
+		if (this.vActionConfigs != null) {
+			configs = this.vActionConfigs
+			configs.forEach(function (v) {
+				v.id = (id++)
+			})
+		}
+
+		var defaultPageBody = `
+
+
+403 Forbidden
+
+`
+
+
+		return {
+			id: id,
+
+			actions: this.vActions,
+			configs: configs,
+			isAdding: false,
+			editingIndex: -1,
+
+			action: null,
+			actionCode: "",
+			actionOptions: {},
+
+			// IPList相关
+			ipListLevels: [],
+
+			// 动作参数
+			blockTimeout: "",
+			blockScope: "global",
+
+			captchaLife: "",
+			get302Life: "",
+			post307Life: "",
+			recordIPType: "black",
+			recordIPLevel: "critical",
+			recordIPTimeout: "",
+			recordIPListId: 0,
+			recordIPListName: "",
+
+			tagTags: [],
+
+			pageStatus: 403,
+			pageBody: defaultPageBody,
+			defaultPageBody: defaultPageBody,
+
+			goGroupName: "",
+			goGroupId: 0,
+			goGroup: null,
+
+			goSetId: 0,
+			goSetName: ""
+		}
+	},
+	watch: {
+		actionCode: function (code) {
+			this.action = this.actions.$find(function (k, v) {
+				return v.code == code
+			})
+			this.actionOptions = {}
+		},
+		blockTimeout: function (v) {
+			v = parseInt(v)
+			if (isNaN(v)) {
+				this.actionOptions["timeout"] = 0
+			} else {
+				this.actionOptions["timeout"] = v
+			}
+		},
+		blockScope: function (v) {
+			this.actionOptions["scope"] = v
+		},
+		captchaLife: function (v) {
+			v = parseInt(v)
+			if (isNaN(v)) {
+				this.actionOptions["life"] = 0
+			} else {
+				this.actionOptions["life"] = v
+			}
+		},
+		get302Life: function (v) {
+			v = parseInt(v)
+			if (isNaN(v)) {
+				this.actionOptions["life"] = 0
+			} else {
+				this.actionOptions["life"] = v
+			}
+		},
+		post307Life: function (v) {
+			v = parseInt(v)
+			if (isNaN(v)) {
+				this.actionOptions["life"] = 0
+			} else {
+				this.actionOptions["life"] = v
+			}
+		},
+		recordIPType: function (v) {
+			this.recordIPListId = 0
+		},
+		recordIPTimeout: function (v) {
+			v = parseInt(v)
+			if (isNaN(v)) {
+				this.actionOptions["timeout"] = 0
+			} else {
+				this.actionOptions["timeout"] = v
+			}
+		},
+		goGroupId: function (groupId) {
+			let group = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
+				return v.id == groupId
+			})
+			this.goGroup = group
+			if (group == null) {
+				this.goGroupName = ""
+			} else {
+				this.goGroupName = group.name
+			}
+			this.goSetId = 0
+			this.goSetName = ""
+		},
+		goSetId: function (setId) {
+			if (this.goGroup == null) {
+				return
+			}
+			let set = this.goGroup.sets.$find(function (k, v) {
+				return v.id == setId
+			})
+			if (set == null) {
+				this.goSetId = 0
+				this.goSetName = ""
+			} else {
+				this.goSetName = set.name
+			}
+		}
+	},
+	methods: {
+		add: function () {
+			this.action = null
+			this.actionCode = "block"
+			this.isAdding = true
+			this.actionOptions = {}
+
+			// 动作参数
+			this.blockTimeout = ""
+			this.blockScope = "global"
+			this.captchaLife = ""
+			this.get302Life = ""
+			this.post307Life = ""
+
+			this.recordIPLevel = "critical"
+			this.recordIPType = "black"
+			this.recordIPTimeout = ""
+			this.recordIPListId = 0
+			this.recordIPListName = ""
+
+			this.tagTags = []
+
+			this.pageStatus = 403
+			this.pageBody = this.defaultPageBody
+
+			this.goGroupName = ""
+			this.goGroupId = 0
+			this.goGroup = null
+
+			this.goSetId = 0
+			this.goSetName = ""
+
+			let that = this
+			this.action = this.vActions.$find(function (k, v) {
+				return v.code == that.actionCode
+			})
+
+			// 滚到界面底部
+			this.scroll()
+		},
+		remove: function (index) {
+			this.isAdding = false
+			this.editingIndex = -1
+			this.configs.$remove(index)
+		},
+		update: function (index, config) {
+			if (this.isAdding && this.editingIndex == index) {
+				this.cancel()
+				return
+			}
+
+			this.add()
+
+			this.isAdding = true
+			this.editingIndex = index
+
+			this.actionCode = config.code
+
+			switch (config.code) {
+				case "block":
+					this.blockTimeout = ""
+					if (config.options.timeout != null || config.options.timeout > 0) {
+						this.blockTimeout = config.options.timeout.toString()
+					}
+					if (config.options.scope != null && config.options.scope.length > 0) {
+						this.blockScope = config.options.scope
+					} else {
+						this.blockScope = "global" // 兼容先前版本遗留的默认值
+					}
+					break
+				case "allow":
+					break
+				case "log":
+					break
+				case "captcha":
+					this.captchaLife = ""
+					if (config.options.life != null || config.options.life > 0) {
+						this.captchaLife = config.options.life.toString()
+					}
+					break
+				case "notify":
+					break
+				case "get_302":
+					this.get302Life = ""
+					if (config.options.life != null || config.options.life > 0) {
+						this.get302Life = config.options.life.toString()
+					}
+					break
+				case "post_307":
+					this.post307Life = ""
+					if (config.options.life != null || config.options.life > 0) {
+						this.post307Life = config.options.life.toString()
+					}
+					break;
+				case "record_ip":
+					if (config.options != null) {
+						this.recordIPLevel = config.options.level
+						this.recordIPType = config.options.type
+						if (config.options.timeout > 0) {
+							this.recordIPTimeout = config.options.timeout.toString()
+						}
+						let that = this
+
+						// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
+						setTimeout(function () {
+							that.recordIPListId = config.options.ipListId
+							that.recordIPListName = config.options.ipListName
+						})
+					}
+					break
+				case "tag":
+					this.tagTags = []
+					if (config.options.tags != null) {
+						this.tagTags = config.options.tags
+					}
+					break
+				case "page":
+					this.pageStatus = 403
+					this.pageBody = this.defaultPageBody
+					if (config.options.status != null) {
+						this.pageStatus = config.options.status
+					}
+					if (config.options.body != null) {
+						this.pageBody = config.options.body
+					}
+
+					break
+				case "go_group":
+					if (config.options != null) {
+						this.goGroupName = config.options.groupName
+						this.goGroupId = config.options.groupId
+						this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
+							return v.id == config.options.groupId
+						})
+					}
+					break
+				case "go_set":
+					if (config.options != null) {
+						this.goGroupName = config.options.groupName
+						this.goGroupId = config.options.groupId
+						this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
+							return v.id == config.options.groupId
+						})
+
+						// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
+						let that = this
+						setTimeout(function () {
+							that.goSetId = config.options.setId
+							if (that.goGroup != null) {
+								let set = that.goGroup.sets.$find(function (k, v) {
+									return v.id == config.options.setId
+								})
+								if (set != null) {
+									that.goSetName = set.name
+								}
+							}
+						})
+					}
+					break
+			}
+
+			// 滚到界面底部
+			this.scroll()
+		},
+		cancel: function () {
+			this.isAdding = false
+			this.editingIndex = -1
+		},
+		confirm: function () {
+			if (this.action == null) {
+				return
+			}
+
+			if (this.actionOptions == null) {
+				this.actionOptions = {}
+			}
+
+			// record_ip
+			if (this.actionCode == "record_ip") {
+				let timeout = parseInt(this.recordIPTimeout)
+				if (isNaN(timeout)) {
+					timeout = 0
+				}
+				if (this.recordIPListId <= 0) {
+					return
+				}
+				this.actionOptions = {
+					type: this.recordIPType,
+					level: this.recordIPLevel,
+					timeout: timeout,
+					ipListId: this.recordIPListId,
+					ipListName: this.recordIPListName
+				}
+			} else if (this.actionCode == "tag") { // tag
+				if (this.tagTags == null || this.tagTags.length == 0) {
+					return
+				}
+				this.actionOptions = {
+					tags: this.tagTags
+				}
+			} else if (this.actionCode == "page") {
+				let pageStatus = this.pageStatus.toString()
+				if (!pageStatus.match(/^\d{3}$/)) {
+					pageStatus = 403
+				} else {
+					pageStatus = parseInt(pageStatus)
+				}
+
+				this.actionOptions = {
+					status: pageStatus,
+					body: this.pageBody
+				}
+			} else if (this.actionCode == "go_group") { // go_group
+				let groupId = this.goGroupId
+				if (typeof (groupId) == "string") {
+					groupId = parseInt(groupId)
+					if (isNaN(groupId)) {
+						groupId = 0
+					}
+				}
+				if (groupId <= 0) {
+					return
+				}
+				this.actionOptions = {
+					groupId: groupId.toString(),
+					groupName: this.goGroupName
+				}
+			} else if (this.actionCode == "go_set") { // go_set
+				let groupId = this.goGroupId
+				if (typeof (groupId) == "string") {
+					groupId = parseInt(groupId)
+					if (isNaN(groupId)) {
+						groupId = 0
+					}
+				}
+
+				let setId = this.goSetId
+				if (typeof (setId) == "string") {
+					setId = parseInt(setId)
+					if (isNaN(setId)) {
+						setId = 0
+					}
+				}
+				if (setId <= 0) {
+					return
+				}
+				this.actionOptions = {
+					groupId: groupId.toString(),
+					groupName: this.goGroupName,
+					setId: setId.toString(),
+					setName: this.goSetName
+				}
+			}
+
+			let options = {}
+			for (let k in this.actionOptions) {
+				if (this.actionOptions.hasOwnProperty(k)) {
+					options[k] = this.actionOptions[k]
+				}
+			}
+			if (this.editingIndex > -1) {
+				this.configs[this.editingIndex] = {
+					id: this.configs[this.editingIndex].id,
+					code: this.actionCode,
+					name: this.action.name,
+					options: options
+				}
+			} else {
+				this.configs.push({
+					id: (this.id++),
+					code: this.actionCode,
+					name: this.action.name,
+					options: options
+				})
+			}
+
+			this.cancel()
+		},
+		removeRecordIPList: function () {
+			this.recordIPListId = 0
+		},
+		selectRecordIPList: function () {
+			let that = this
+			teaweb.popup("/servers/iplists/selectPopup?type=" + this.recordIPType, {
+				width: "50em",
+				height: "30em",
+				callback: function (resp) {
+					that.recordIPListId = resp.data.list.id
+					that.recordIPListName = resp.data.list.name
+				}
+			})
+		},
+		changeTags: function (tags) {
+			this.tagTags = tags
+		},
+		loadJS: function (callback) {
+			if (typeof Sortable != "undefined") {
+				callback()
+				return
+			}
+
+			// 引入js
+			let jsFile = document.createElement("script")
+			jsFile.setAttribute("src", "/js/sortable.min.js")
+			jsFile.addEventListener("load", function () {
+				callback()
+			})
+			document.head.appendChild(jsFile)
+		},
+		scroll: function () {
+			setTimeout(function () {
+				let mainDiv = document.getElementsByClassName("main")
+				if (mainDiv.length > 0) {
+					mainDiv[0].scrollTo(0, 1000)
+				}
+			}, 10)
+		}
+	},
+	template: `
+	
+	
 
+		
+			{{config.name}} 
({{config.code.toUpperCase()}})  
+			
+			
+			
:有效期{{config.options.timeout}}秒 
+			
+			
+			
:有效期{{config.options.life}}秒 
+			
+			
+			
:有效期{{config.options.life}}秒 
+			
+			
+			
:有效期{{config.options.life}}秒 
+			
+			
+			
:{{config.options.ipListName}} 
+			
+			
+			
:{{config.options.tags.join(", ")}} 
+			
+			
+			
:[{{config.options.status}}] 
+			
+			
+			
:{{config.options.groupName}} 
+			
+			
+			
:{{config.options.groupName}} / {{config.options.setName}} 
+			
+			
+			
+				  
+				[所有服务] 
+				[当前服务] 
+			 
+			
+			
+			   
        
+		
+		
+	
+	
+	
+		+ 
+	
+	
+
 `
+})
+
+// 认证设置
+Vue.component("http-auth-config-box", {
+	props: ["v-auth-config", "v-is-location"],
+	data: function () {
+		let authConfig = this.vAuthConfig
+		if (authConfig == null) {
+			authConfig = {
+				isPrior: false,
+				isOn: false
+			}
+		}
+		if (authConfig.policyRefs == null) {
+			authConfig.policyRefs = []
+		}
+		return {
+			authConfig: authConfig
+		}
+	},
+	methods: {
+		isOn: function () {
+			return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
+		},
+		add: function () {
+			let that = this
+			teaweb.popup("/servers/server/settings/access/createPopup", {
+				callback: function (resp) {
+					that.authConfig.policyRefs.push(resp.data.policyRef)
+				},
+				height: "28em"
+			})
+		},
+		update: function (index, policyId) {
+			let that = this
+			teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
+				callback: function (resp) {
+					teaweb.success("保存成功", function () {
+						teaweb.reload()
+					})
+				},
+				height: "28em"
+			})
+		},
+		remove: function (index) {
+			this.authConfig.policyRefs.$remove(index)
+		},
+		methodName: function (methodType) {
+			switch (methodType) {
+				case "basicAuth":
+					return "BasicAuth"
+				case "subRequest":
+					return "子请求"
+			}
+			return ""
+		}
+	},
+	template: `
+
 
+
+
+
+
+	
认证方式 
+	
+		
+			
+				名称 
+				认证方法 
+				参数 
+				状态 
+				操作 
+			 
+		 
+		
+			
+				{{ref.authPolicy.name}} 
+				
+					{{methodName(ref.authPolicy.type)}}
+				 
+				
+					{{ref.authPolicy.params.users.length}}个用户 
+					
+						[{{ref.authPolicy.params.method}}] 
+						{{ref.authPolicy.params.url}}
+					 
+				 
+				
+					 
+				 
+				
+					修改   
+					删除 
+				 
+			 
+		 
+	
+	
+添加认证方式 
+
+
+
 `
+})
+
+Vue.component("user-selector", {
+	mounted: function () {
+		let that = this
+
+		Tea.action("/servers/users/options")
+			.post()
+			.success(function (resp) {
+				that.users = resp.data.users
+			})
+	},
+	props: ["v-user-id"],
+	data: function () {
+		let userId = this.vUserId
+		if (userId == null) {
+			userId = 0
+		}
+		return {
+			users: [],
+			userId: userId
+		}
+	},
+	watch: {
+		userId: function (v) {
+			this.$emit("change", v)
+		}
+	},
+	template: `
+	
+		[选择用户] 
+		{{user.fullname}} ({{user.username}}) 
+	 
+
`
+})
+
+Vue.component("http-header-policy-box", {
+	props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"],
+	data: function () {
+		let type = "request"
+		let hash = window.location.hash
+		if (hash == "#response") {
+			type = "response"
+		}
+
+		// ref
+		let requestHeaderRef = this.vRequestHeaderRef
+		if (requestHeaderRef == null) {
+			requestHeaderRef = {
+				isPrior: false,
+				isOn: true,
+				headerPolicyId: 0
+			}
+		}
+
+		let responseHeaderRef = this.vResponseHeaderRef
+		if (responseHeaderRef == null) {
+			responseHeaderRef = {
+				isPrior: false,
+				isOn: true,
+				headerPolicyId: 0
+			}
+		}
+
+		// 请求相关
+		let requestSettingHeaders = []
+		let requestDeletingHeaders = []
+
+		let requestPolicy = this.vRequestHeaderPolicy
+		if (requestPolicy != null) {
+			if (requestPolicy.setHeaders != null) {
+				requestSettingHeaders = requestPolicy.setHeaders
+			}
+			if (requestPolicy.deleteHeaders != null) {
+				requestDeletingHeaders = requestPolicy.deleteHeaders
+			}
+		}
+
+		// 响应相关
+		let responseSettingHeaders = []
+		let responseDeletingHeaders = []
+
+		let responsePolicy = this.vResponseHeaderPolicy
+		if (responsePolicy != null) {
+			if (responsePolicy.setHeaders != null) {
+				responseSettingHeaders = responsePolicy.setHeaders
+			}
+			if (responsePolicy.deleteHeaders != null) {
+				responseDeletingHeaders = responsePolicy.deleteHeaders
+			}
+		}
+		
+		return {
+			type: type,
+			typeName: (type == "request") ? "请求" : "响应",
+			requestHeaderRef: requestHeaderRef,
+			responseHeaderRef: responseHeaderRef,
+			requestSettingHeaders: requestSettingHeaders,
+			requestDeletingHeaders: requestDeletingHeaders,
+			responseSettingHeaders: responseSettingHeaders,
+			responseDeletingHeaders: responseDeletingHeaders
+		}
+	},
+	methods: {
+		selectType: function (type) {
+			this.type = type
+			window.location.hash = "#" + type
+			window.location.reload()
+		},
+		addSettingHeader: function (policyId) {
+			teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId, {
+				callback: function () {
+					window.location.reload()
+				}
+			})
+		},
+		addDeletingHeader: function (policyId, type) {
+			teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
+				callback: function () {
+					window.location.reload()
+				}
+			})
+		},
+		updateSettingPopup: function (policyId, headerId) {
+			teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId, {
+				callback: function () {
+					window.location.reload()
+				}
+			})
+		},
+		deleteDeletingHeader: function (policyId, headerName) {
+			teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
+				Tea.action("/servers/server/settings/headers/deleteDeletingHeader")
+					.params({
+						headerPolicyId: policyId,
+						headerName: headerName
+					})
+					.post()
+					.refresh()
+			})
+		},
+		deleteHeader: function (policyId, type, headerId) {
+			teaweb.confirm("确定要删除此Header吗?", function () {
+					this.$post("/servers/server/settings/headers/delete")
+						.params({
+							headerPolicyId: policyId,
+							type: type,
+							headerId: headerId
+						})
+						.refresh()
+				}
+			)
+		}
+	},
+	template: `
+	
+	
+	
+	
+	
+	
+	
+	
+	
+	
+		
+        	
+        	
由于已经在当前服务分组 中进行了对应的配置,在这里的配置将不会生效。 
+    	
+    	
+		
+			
+			
+				
+					
+						名称 
+						值 
+						操作 
+					 
+				 
+				
+					{{header.name}} 
+					{{header.value}} 
+					修改    删除   
+				 
+			
+			
+			
删除请求Header 
+			
+			
+			
+				需要删除的Header 
+				
+					
+					+ 
+				 
+			
+		
			
+	
+	
+	
+	
+	
+	
+		
+        	
+        	
由于已经在当前服务分组 中进行了对应的配置,在这里的配置将不会生效。 
+    	
+    	
+			
+			
+			
+			
+				
+					
+						名称 
+						值 
+						操作 
+					 
+				 
+				
+					{{header.name}} 
+					{{header.value}} 
+					修改    删除   
+				 
+			
+			
+			
删除响应Header 
+			
+			
+			
+				需要删除的Header 
+				
+					
+					+ 
+				 
+			
+		
			
+	
+	
+
 `
+})
+
+Vue.component("http-cache-policy-selector", {
+	props: ["v-cache-policy"],
+	mounted: function () {
+		let that = this
+		Tea.action("/servers/components/cache/count")
+			.post()
+			.success(function (resp) {
+				that.count = resp.data.count
+			})
+	},
+	data: function () {
+		let cachePolicy = this.vCachePolicy
+		return {
+			count: 0,
+			cachePolicy: cachePolicy
+		}
+	},
+	methods: {
+		remove: function () {
+			this.cachePolicy = null
+		},
+		select: function () {
+			let that = this
+			teaweb.popup("/servers/components/cache/selectPopup", {
+				callback: function (resp) {
+					that.cachePolicy = resp.data.cachePolicy
+				}
+			})
+		},
+		create: function () {
+			let that = this
+			teaweb.popup("/servers/components/cache/createPopup", {
+				height: "26em",
+				callback: function (resp) {
+					that.cachePolicy = resp.data.cachePolicy
+				}
+			})
+		}
+	},
+	template: `
+	
+		
+		{{cachePolicy.name}}   
   
+	
+	
+
 `
+})
+
+Vue.component("http-pages-and-shutdown-box", {
+	props: ["v-pages", "v-shutdown-config", "v-is-location"],
+	data: function () {
+		let pages = []
+		if (this.vPages != null) {
+			pages = this.vPages
+		}
+		let shutdownConfig = {
+			isPrior: false,
+			isOn: false,
+			bodyType: "url",
+			url: "",
+			body: "",
+			status: 0
+		}
+		if (this.vShutdownConfig != null) {
+			if (this.vShutdownConfig.body == null) {
+				this.vShutdownConfig.body = ""
+			}
+			if (this.vShutdownConfig.bodyType == null) {
+				this.vShutdownConfig.bodyType = "url"
+			}
+			shutdownConfig = this.vShutdownConfig
+		}
+
+		let shutdownStatus = ""
+		if (shutdownConfig.status > 0) {
+			shutdownStatus = shutdownConfig.status.toString()
+		}
+
+		return {
+			pages: pages,
+			shutdownConfig: shutdownConfig,
+			shutdownStatus: shutdownStatus
+		}
+	},
+	watch: {
+		shutdownStatus: function (status) {
+			let statusInt = parseInt(status)
+			if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) {
+				this.shutdownConfig.status = statusInt
+			} else {
+				this.shutdownConfig.status = 0
+			}
+		}
+	},
+	methods: {
+		addPage: function () {
+			let that = this
+			teaweb.popup("/servers/server/settings/pages/createPopup", {
+				height: "26em",
+				callback: function (resp) {
+					that.pages.push(resp.data.page)
+				}
+			})
+		},
+		updatePage: function (pageIndex, pageId) {
+			let that = this
+			teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, {
+				height: "26em",
+				callback: function (resp) {
+					Vue.set(that.pages, pageIndex, resp.data.page)
+				}
+			})
+		},
+		removePage: function (pageIndex) {
+			let that = this
+			teaweb.confirm("确定要移除此页面吗?", function () {
+				that.pages.$remove(pageIndex)
+			})
+		},
+		addShutdownHTMLTemplate: function () {
+			this.shutdownConfig.body  = `
+
+
+\t升级中 
+\t 
+
+
+
+网站升级中 
+为了给您提供更好的服务,我们正在升级网站,请稍后重新访问。
+
+
+
+
+`
+		}
+	},
+	template: `
+
+
+
+	
+		特殊页面 
+		
+			
+				
+					{{page.status}} -> 
{{page.url}} [HTML内容]    
+				
+				
+			
 
+			
+				+ 
+			
+			
+		 
+	 	
+	
+		临时关闭页面 
+		
+			
+		 
+	 
+
+
+
 `
+})
+
+// 压缩配置
+Vue.component("http-compression-config-box", {
+	props: ["v-compression-config", "v-is-location", "v-is-group"],
+	mounted: function () {
+		let that = this
+		sortLoad(function () {
+			that.initSortableTypes()
+		})
+	},
+	data: function () {
+		let config = this.vCompressionConfig
+		if (config == null) {
+			config = {
+				isPrior: false,
+				isOn: false,
+				useDefaultTypes: true,
+				types: ["brotli", "gzip", "deflate"],
+				level: 5,
+				decompressData: false,
+				gzipRef: null,
+				deflateRef: null,
+				brotliRef: null,
+				minLength: {count: 0, "unit": "kb"},
+				maxLength: {count: 0, "unit": "kb"},
+				mimeTypes: ["text/*", "application/*", "font/*"],
+				extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"],
+				conds: null
+			}
+		}
+
+		if (config.types == null) {
+			config.types = []
+		}
+		if (config.mimeTypes == null) {
+			config.mimeTypes = []
+		}
+		if (config.extensions == null) {
+			config.extensions = []
+		}
+
+		let allTypes = [
+			{
+				name: "Gzip",
+				code: "gzip",
+				isOn: true
+			},
+			{
+				name: "Deflate",
+				code: "deflate",
+				isOn: true
+			},
+			{
+				name: "Brotli",
+				code: "brotli",
+				isOn: true
+			}
+		]
+
+		let configTypes = []
+		config.types.forEach(function (typeCode) {
+			allTypes.forEach(function (t) {
+				if (typeCode == t.code) {
+					t.isOn = true
+					configTypes.push(t)
+				}
+			})
+		})
+		allTypes.forEach(function (t) {
+			if (!config.types.$contains(t.code)) {
+				t.isOn = false
+				configTypes.push(t)
+			}
+		})
+
+		return {
+			config: config,
+			moreOptionsVisible: false,
+			allTypes: configTypes
+		}
+	},
+	watch: {
+		"config.level": function (v) {
+			let level = parseInt(v)
+			if (isNaN(level)) {
+				level = 1
+			} else if (level < 1) {
+				level = 1
+			} else if (level > 10) {
+				level = 10
+			}
+			this.config.level = level
+		}
+	},
+	methods: {
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
+		},
+		changeExtensions: function (values) {
+			values.forEach(function (v, k) {
+				if (v.length > 0 && v[0] != ".") {
+					values[k] = "." + v
+				}
+			})
+			this.config.extensions = values
+		},
+		changeMimeTypes: function (values) {
+			this.config.mimeTypes = values
+		},
+		changeAdvancedVisible: function () {
+			this.moreOptionsVisible = !this.moreOptionsVisible
+		},
+		changeConds: function (conds) {
+			this.config.conds = conds
+		},
+		changeType: function () {
+			this.config.types = []
+			let that = this
+			this.allTypes.forEach(function (v) {
+				if (v.isOn) {
+					that.config.types.push(v.code)
+				}
+			})
+		},
+		initSortableTypes: function () {
+			let box = document.querySelector("#compression-types-box")
+			let that = this
+			Sortable.create(box, {
+				draggable: ".checkbox",
+				handle: ".icon.handle",
+				onStart: function () {
+
+				},
+				onUpdate: function (event) {
+					let checkboxes = box.querySelectorAll(".checkbox")
+					let codes = []
+					checkboxes.forEach(function (checkbox) {
+						let code = checkbox.getAttribute("data-code")
+						codes.push(code)
+					})
+					that.config.types = codes
+				}
+			})
+		}
+	},
+	template: ``
+})
+
+Vue.component("firewall-event-level-options", {
+    props: ["v-value"],
+    mounted: function () {
+        let that = this
+        Tea.action("/ui/eventLevelOptions")
+            .post()
+            .success(function (resp) {
+                that.levels = resp.data.eventLevels
+                that.change()
+            })
+    },
+    data: function () {
+        let value = this.vValue
+        if (value == null || value.length == 0) {
+            value = "" // 不要给默认值,因为黑白名单等默认值均有不同
+        }
+
+        return {
+            levels: [],
+            description: "",
+            level: value
+        }
+    },
+    methods: {
+        change: function () {
+            this.$emit("change")
+
+            let that = this
+            let l = this.levels.$find(function (k, v) {
+                return v.code == that.level
+            })
+            if (l != null) {
+                this.description = l.description
+            } else {
+                this.description = ""
+            }
+        }
+    },
+    template: `
+    
+        {{level.name}} 
+     
+    
+
`
+})
+
+Vue.component("prior-checkbox", {
+	props: ["v-config"],
+	data: function () {
+		return {
+			isPrior: this.vConfig.isPrior
+		}
+	},
+	watch: {
+		isPrior: function (v) {
+			this.vConfig.isPrior = v
+		}
+	},
+	template: `
+	
+		打开独立配置 
+		
+			
+				 
+				 
+			
+			
+		 
+	 
+ `
+})
+
+Vue.component("http-charsets-box", {
+	props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let charsetConfig = this.vCharsetConfig
+		if (charsetConfig == null) {
+			charsetConfig = {
+				isPrior: false,
+				isOn: false,
+				charset: "",
+				isUpper: false
+			}
+		}
+		return {
+			charsetConfig: charsetConfig,
+			advancedVisible: false
+		}
+	},
+	methods: {
+		changeAdvancedVisible: function (v) {
+			this.advancedVisible = v
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-access-log-box", {
+	props: ["v-access-log", "v-keyword", "v-show-server-link"],
+	data: function () {
+		let accessLog = this.vAccessLog
+		if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) {
+			if (accessLog.scheme == "http") {
+				accessLog.scheme = "ws"
+			} else if (accessLog.scheme == "https") {
+				accessLog.scheme = "wss"
+			}
+		}
+
+		return {
+			accessLog: accessLog
+		}
+	},
+	methods: {
+		formatCost: function (seconds) {
+			var s = (seconds * 1000).toString();
+			var pieces = s.split(".");
+			if (pieces.length < 2) {
+				return s;
+			}
+
+			return pieces[0] + "." + pieces[1].substr(0, 3);
+		},
+		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("/servers/server/log/viewPopup?requestId=" + requestId, {
+				width: "50em",
+				height: "28em",
+				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: `
+	
[{{accessLog.node.name}}节点 ] 
+	
[服务] 
+	
[{{accessLog.region}}]  {{accessLog.remoteAddr}}  [{{accessLog.timeLocal}}] 
"{{accessLog.requestMethod}}  {{accessLog.scheme}}://{{accessLog.host}} {{accessLog.requestURI}}      {{accessLog.proto}}"   {{accessLog.status}}  cache hit  waf {{accessLog.firewallActions}}  - {{tag}}   - 耗时:{{formatCost(accessLog.requestTime)}} ms 
  ({{accessLog.humanTime}}) 
+	  
+
 `
+})
+
+Vue.component("http-access-log-config-box", {
+	props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"],
+	data: function () {
+		let that = this
+
+		// 初始化
+		setTimeout(function () {
+			that.changeFields()
+		}, 100)
+
+		let accessLog = {
+			isPrior: false,
+			isOn: false,
+			fields: [],
+			status1: true,
+			status2: true,
+			status3: true,
+			status4: true,
+			status5: true,
+
+            firewallOnly: false
+		}
+		if (this.vAccessLogConfig != null) {
+			accessLog = this.vAccessLogConfig
+		}
+
+		this.vFields.forEach(function (v) {
+			if (that.vAccessLogConfig == null) { // 初始化默认值
+				v.isChecked = that.vDefaultFieldCodes.$contains(v.code)
+			} else {
+				v.isChecked = accessLog.fields.$contains(v.code)
+			}
+		})
+
+		return {
+			accessLog: accessLog
+		}
+	},
+	methods: {
+		changeFields: function () {
+			this.accessLog.fields = this.vFields.filter(function (v) {
+				return v.isChecked
+			}).map(function (v) {
+				return v.code
+			})
+		}
+	},
+	template: `
+	
+	
+	
+	
+        
WAF相关 
+        
+            
+                是否只记录WAF相关日志 
+                
+                     
+                    
+                 
+             
+        
+    
+	
+
 `
+})
+
+// 显示流量限制说明
+Vue.component("traffic-limit-view", {
+	props: ["v-traffic-limit"],
+	data: function () {
+		return {
+			config: this.vTrafficLimit
+		}
+	},
+	template: `
+	
+		日流量限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}} 
+		月流量限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}} 
+	
+	
没有限制。 
+
 `
+})
+
+// 基本认证用户配置
+Vue.component("http-auth-basic-auth-user-box", {
+	props: ["v-users"],
+	data: function () {
+		let users = this.vUsers
+		if (users == null) {
+			users = []
+		}
+		return {
+			users: users,
+			isAdding: false,
+			updatingIndex: -1,
+
+			username: "",
+			password: ""
+		}
+	},
+	methods: {
+		add: function () {
+			this.isAdding = true
+			this.username = ""
+			this.password = ""
+
+			let that = this
+			setTimeout(function () {
+				that.$refs.username.focus()
+			}, 100)
+		},
+		cancel: function () {
+			this.isAdding = false
+			this.updatingIndex = -1
+		},
+		confirm: function () {
+			let that = this
+			if (this.username.length == 0) {
+				teaweb.warn("请输入用户名", function () {
+					that.$refs.username.focus()
+				})
+				return
+			}
+			if (this.password.length == 0) {
+				teaweb.warn("请输入密码", function () {
+					that.$refs.password.focus()
+				})
+				return
+			}
+			if (this.updatingIndex < 0) {
+				this.users.push({
+					username: this.username,
+					password: this.password
+				})
+			} else {
+				this.users[this.updatingIndex].username = this.username
+				this.users[this.updatingIndex].password = this.password
+			}
+			this.cancel()
+		},
+		update: function (index, user) {
+			this.updatingIndex = index
+
+			this.isAdding = true
+			this.username = user.username
+			this.password = user.password
+
+			let that = this
+			setTimeout(function () {
+				that.$refs.username.focus()
+			}, 100)
+		},
+		remove: function (index) {
+			this.users.$remove(index)
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-location-labels", {
+	props: ["v-location-config", "v-server-id"],
+	data: function () {
+		return {
+			location: this.vLocationConfig
+		}
+	},
+	methods: {
+		// 判断是否已启用某配置
+		configIsOn: function (config) {
+			return config != null && config.isPrior && config.isOn
+		},
+
+		refIsOn: function (ref, config) {
+			return this.configIsOn(ref) && config != null && config.isOn
+		},
+
+		len: function (arr) {
+			return (arr == null) ? 0 : arr.length
+		},
+		url: function (path) {
+			return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id
+		}
+	},
+	template: `	
+	
+	
{{location.name}} 
+	
BREAK 
+	
+	
+	
自动跳转HTTPS 
+	
+	
+	
文档根目录 
+	
+	
+	
反向代理 
+	
+	
+	
+	
+	
+	
CACHE 
+	
+	
+	
{{location.web.charset.charset}} 
+	
+	
+	
+	
+	
+	
+	
+	
+	
Gzip:{{location.web.gzip.level}} 
+	
+	
+	
请求Header 
+	
响应Header 
+	
+	
+	
Websocket 
+	
+	
+	
+		
PAGE [状态码{{page.status[0]}}] -> {{page.url}} 
+	
+	
+		临时关闭 
+	
+	
+	
+	
+		
+			REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}} 
+		
+	
+
 `
+})
+
+Vue.component("http-location-labels-label", {
+	props: ["v-class", "v-href"],
+	template: ` `
+})
+
+Vue.component("http-gzip-box", {
+	props: ["v-gzip-config", "v-gzip-ref", "v-is-location"],
+	data: function () {
+		let gzip = this.vGzipConfig
+		if (gzip == null) {
+			gzip = {
+				isOn: true,
+				level: 0,
+				minLength: null,
+				maxLength: null,
+				conds: null
+			}
+		}
+
+		return {
+			gzip: gzip,
+			advancedVisible: false
+		}
+	},
+	methods: {
+		isOn: function () {
+			return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.isOn
+		},
+		changeAdvancedVisible: function (v) {
+			this.advancedVisible = v
+		}
+	},
+	template: ``
+})
+
+Vue.component("ssl-certs-view", {
+	props: ["v-certs"],
+	data: function () {
+		let certs = this.vCerts
+		if (certs == null) {
+			certs = []
+		}
+		return {
+			certs: certs
+		}
+	},
+	methods: {
+		// 格式化时间
+		formatTime: function (timestamp) {
+			return new Date(timestamp * 1000).format("Y-m-d")
+		},
+
+		// 查看详情
+		viewCert: function (certId) {
+			teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
+				height: "28em",
+				width: "48em"
+			})
+		}
+	},
+	template: `
+	
+		
+			{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}  
+		
+	
+
 `
+})
+
+Vue.component("reverse-proxy-box", {
+	props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"],
+	data: function () {
+		let reverseProxyRef = this.vReverseProxyRef
+		if (reverseProxyRef == null) {
+			reverseProxyRef = {
+				isPrior: false,
+				isOn: false,
+				reverseProxyId: 0
+			}
+		}
+
+		let reverseProxyConfig = this.vReverseProxyConfig
+		if (reverseProxyConfig == null) {
+			reverseProxyConfig = {
+				requestPath: "",
+				stripPrefix: "",
+				requestURI: "",
+				requestHost: "",
+				requestHostType: 0,
+				addHeaders: [],
+				connTimeout: {count: 0, unit: "second"},
+				readTimeout: {count: 0, unit: "second"},
+				idleTimeout: {count: 0, unit: "second"},
+				maxConns: 0,
+				maxIdleConns: 0
+			}
+		}
+		if (reverseProxyConfig.addHeaders == null) {
+			reverseProxyConfig.addHeaders = []
+		}
+		if (reverseProxyConfig.connTimeout == null) {
+			reverseProxyConfig.connTimeout = {count: 0, unit: "second"}
+		}
+		if (reverseProxyConfig.readTimeout == null) {
+			reverseProxyConfig.readTimeout = {count: 0, unit: "second"}
+		}
+		if (reverseProxyConfig.idleTimeout == null) {
+			reverseProxyConfig.idleTimeout = {count: 0, unit: "second"}
+		}
+
+		if (reverseProxyConfig.proxyProtocol == null) {
+			// 如果直接赋值Vue将不会触发变更通知
+			Vue.set(reverseProxyConfig, "proxyProtocol", {
+				isOn: false,
+				version: 1
+			})
+		}
+
+		let forwardHeaders = [
+			{
+				name: "X-Real-IP",
+				isChecked: false
+			},
+			{
+				name: "X-Forwarded-For",
+				isChecked: false
+			},
+			{
+				name: "X-Forwarded-By",
+				isChecked: false
+			},
+			{
+				name: "X-Forwarded-Host",
+				isChecked: false
+			},
+			{
+				name: "X-Forwarded-Proto",
+				isChecked: false
+			}
+		]
+		forwardHeaders.forEach(function (v) {
+			v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name)
+		})
+
+		return {
+			reverseProxyRef: reverseProxyRef,
+			reverseProxyConfig: reverseProxyConfig,
+			advancedVisible: false,
+			family: this.vFamily,
+			forwardHeaders: forwardHeaders
+		}
+	},
+	watch: {
+		"reverseProxyConfig.requestHostType": function (v) {
+			let requestHostType = parseInt(v)
+			if (isNaN(requestHostType)) {
+				requestHostType = 0
+			}
+			this.reverseProxyConfig.requestHostType = requestHostType
+		},
+		"reverseProxyConfig.connTimeout.count": function (v) {
+			let count = parseInt(v)
+			if (isNaN(count) || count < 0) {
+				count = 0
+			}
+			this.reverseProxyConfig.connTimeout.count = count
+		},
+		"reverseProxyConfig.readTimeout.count": function (v) {
+			let count = parseInt(v)
+			if (isNaN(count) || count < 0) {
+				count = 0
+			}
+			this.reverseProxyConfig.readTimeout.count = count
+		},
+		"reverseProxyConfig.idleTimeout.count": function (v) {
+			let count = parseInt(v)
+			if (isNaN(count) || count < 0) {
+				count = 0
+			}
+			this.reverseProxyConfig.idleTimeout.count = count
+		},
+		"reverseProxyConfig.maxConns": function (v) {
+			let maxConns = parseInt(v)
+			if (isNaN(maxConns) || maxConns < 0) {
+				maxConns = 0
+			}
+			this.reverseProxyConfig.maxConns = maxConns
+		},
+		"reverseProxyConfig.maxIdleConns": function (v) {
+			let maxIdleConns = parseInt(v)
+			if (isNaN(maxIdleConns) || maxIdleConns < 0) {
+				maxIdleConns = 0
+			}
+			this.reverseProxyConfig.maxIdleConns = maxIdleConns
+		},
+		"reverseProxyConfig.proxyProtocol.version": function (v) {
+			let version = parseInt(v)
+			if (isNaN(version)) {
+				version = 1
+			}
+			this.reverseProxyConfig.proxyProtocol.version = version
+		}
+	},
+	methods: {
+		isOn: function () {
+			if (this.vIsLocation || this.vIsGroup) {
+				return this.reverseProxyRef.isPrior && this.reverseProxyRef.isOn
+			}
+			return this.reverseProxyRef.isOn
+		},
+		changeAdvancedVisible: function (v) {
+			this.advancedVisible = v
+		},
+		changeAddHeader: function () {
+			this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) {
+				return v.isChecked
+			}).map(function (v) {
+				return v.name
+			})
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-firewall-param-filters-box", {
+	props: ["v-filters"],
+	data: function () {
+		let filters = this.vFilters
+		if (filters == null) {
+			filters = []
+		}
+
+		return {
+			filters: filters,
+			isAdding: false,
+			options: [
+				{name: "MD5", code: "md5"},
+				{name: "URLEncode", code: "urlEncode"},
+				{name: "URLDecode", code: "urlDecode"},
+				{name: "BASE64Encode", code: "base64Encode"},
+				{name: "BASE64Decode", code: "base64Decode"},
+				{name: "UNICODE编码", code: "unicodeEncode"},
+				{name: "UNICODE解码", code: "unicodeDecode"},
+				{name: "HTML实体编码", code: "htmlEscape"},
+				{name: "HTML实体解码", code: "htmlUnescape"},
+				{name: "计算长度", code: "length"},
+				{name: "十六进制->十进制", "code": "hex2dec"},
+				{name: "十进制->十六进制", "code": "dec2hex"},
+				{name: "SHA1", "code": "sha1"},
+				{name: "SHA256", "code": "sha256"}
+			],
+			addingCode: ""
+		}
+	},
+	methods: {
+		add: function () {
+			this.isAdding = true
+			this.addingCode = ""
+		},
+		confirm: function () {
+			if (this.addingCode.length == 0) {
+				return
+			}
+			let that = this
+			this.filters.push(this.options.$find(function (k, v) {
+				return (v.code == that.addingCode)
+			}))
+			this.isAdding = false
+		},
+		cancel: function () {
+			this.isAdding = false
+		},
+		remove: function (index) {
+			this.filters.$remove(index)
+		}
+	},
+	template: `
+		
+		
+		
+			
+				
+					
+						[请选择] 
+						{{option.name}} 
+					 
+				
+				
+			
+		
+		
+			+ 
+		
+		
+
 `
+})
+
+Vue.component("http-remote-addr-config-box", {
+	props: ["v-remote-addr-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let config = this.vRemoteAddrConfig
+		if (config == null) {
+			config = {
+				isPrior: false,
+				isOn: false,
+				value: "${rawRemoteAddr}",
+				isCustomized: false
+			}
+		}
+
+		let optionValue = ""
+		if (!config.isCustomized && (config.value == "${remoteAddr}" || config.value == "${rawRemoteAddr}")) {
+			optionValue = config.value
+		}
+
+		return {
+			config: config,
+			options: [
+				{
+					name: "直接获取",
+					description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候可以直接从连接中读取到真实的IP地址。",
+					value: "${rawRemoteAddr}"
+				},
+				{
+					name: "从上级代理中获取",
+					description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址。",
+					value: "${remoteAddr}"
+				},
+				{
+					name: "[自定义]",
+					description: "通过自定义变量来获取客户端真实的IP地址。",
+					value: ""
+				}
+			],
+			optionValue: optionValue
+		}
+	},
+	methods: {
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
+		},
+		changeOptionValue: function () {
+			if (this.optionValue.length > 0) {
+				this.config.value = this.optionValue
+				this.config.isCustomized = false
+			} else {
+				this.config.isCustomized = true
+			}
+		}
+	},
+	template: ``
+})
+
+// 访问日志搜索框
+Vue.component("http-access-log-search-box", {
+	props: ["v-ip", "v-domain", "v-keyword"],
+	data: function () {
+		let ip = this.vIp
+		if (ip == null) {
+			ip = ""
+		}
+
+		let domain = this.vDomain
+		if (domain == null) {
+			domain = ""
+		}
+
+		let keyword = this.vKeyword
+		if (keyword == null) {
+			keyword = ""
+		}
+
+		return {
+			ip: ip,
+			domain: domain,
+			keyword: keyword
+		}
+	},
+	methods: {
+		cleanIP: function () {
+			this.ip = ""
+			this.submit()
+		},
+		cleanDomain: function () {
+			this.domain = ""
+			this.submit()
+		},
+		cleanKeyword: function () {
+			this.keyword = ""
+			this.submit()
+		},
+		submit: function () {
+			let parent = this.$el.parentNode
+			while (true) {
+				if (parent == null) {
+					break
+				}
+				if (parent.tagName == "FORM") {
+					break
+				}
+				parent = parent.parentNode
+			}
+			if (parent != null) {
+				setTimeout(function () {
+					parent.submit()
+				}, 500)
+			}
+		}
+	},
+	template: ``
+})
+
+// 显示指标对象名
+Vue.component("metric-key-label", {
+	props: ["v-key"],
+	data: function () {
+		return {
+			keyDefs: window.METRIC_HTTP_KEYS
+		}
+	},
+	methods: {
+		keyName: function (key) {
+			let that = this
+			let subKey = ""
+			let def = this.keyDefs.$find(function (k, v) {
+				if (v.code == key) {
+					return true
+				}
+				if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) {
+					subKey = that.getSubKey("arg.", key)
+					return true
+				}
+				if (key.startsWith("${header.") && v.code.startsWith("${header.")) {
+					subKey = that.getSubKey("header.", key)
+					return true
+				}
+				if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) {
+					subKey = that.getSubKey("cookie.", key)
+					return true
+				}
+				return false
+			})
+			if (def != null) {
+				if (subKey.length > 0) {
+					return def.name + ": " + subKey
+				}
+				return def.name
+			}
+			return key
+		},
+		getSubKey: function (prefix, key) {
+			prefix = "${" + prefix
+			let index = key.indexOf(prefix)
+			if (index >= 0) {
+				key = key.substring(index + prefix.length)
+				key = key.substring(0, key.length - 1)
+				return key
+			}
+			return ""
+		}
+	},
+	template: `
+	{{keyName(this.vKey)}}
+
`
+})
+
+// 指标对象
+Vue.component("metric-keys-config-box", {
+	props: ["v-keys"],
+	data: function () {
+		let keys = this.vKeys
+		if (keys == null) {
+			keys = []
+		}
+		return {
+			keys: keys,
+			isAdding: false,
+			key: "",
+			subKey: "",
+			keyDescription: "",
+
+			keyDefs: window.METRIC_HTTP_KEYS
+		}
+	},
+	watch: {
+		keys: function () {
+			this.$emit("change", this.keys)
+		}
+	},
+	methods: {
+		cancel: function () {
+			this.key = ""
+			this.subKey = ""
+			this.keyDescription = ""
+			this.isAdding = false
+		},
+		confirm: function () {
+			if (this.key.length == 0) {
+				return
+			}
+
+			if (this.key.indexOf(".NAME") > 0) {
+				if (this.subKey.length == 0) {
+					teaweb.warn("请输入参数值")
+					return
+				}
+				this.key = this.key.replace(".NAME", "." + this.subKey)
+			}
+			this.keys.push(this.key)
+			this.cancel()
+		},
+		add: function () {
+			this.isAdding = true
+			let that = this
+			setTimeout(function () {
+				if (that.$refs.key != null) {
+					that.$refs.key.focus()
+				}
+			}, 100)
+		},
+		remove: function (index) {
+			this.keys.$remove(index)
+		},
+		changeKey: function () {
+			if (this.key.length == 0) {
+				return
+			}
+			let that = this
+			let def = this.keyDefs.$find(function (k, v) {
+				return v.code == that.key
+			})
+			if (def != null) {
+				this.keyDescription = def.description
+			}
+		},
+		keyName: function (key) {
+			let that = this
+			let subKey = ""
+			let def = this.keyDefs.$find(function (k, v) {
+				if (v.code == key) {
+					return true
+				}
+				if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) {
+					subKey = that.getSubKey("arg.", key)
+					return true
+				}
+				if (key.startsWith("${header.") && v.code.startsWith("${header.")) {
+					subKey = that.getSubKey("header.", key)
+					return true
+				}
+				if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) {
+					subKey = that.getSubKey("cookie.", key)
+					return true
+				}
+				return false
+			})
+			if (def != null) {
+				if (subKey.length > 0) {
+					return def.name + ": " + subKey
+				}
+				return def.name
+			}
+			return key
+		},
+		getSubKey: function (prefix, key) {
+			prefix = "${" + prefix
+			let index = key.indexOf(prefix)
+			if (index >= 0) {
+				key = key.substring(index + prefix.length)
+				key = key.substring(0, key.length - 1)
+				return key
+			}
+			return ""
+		}
+	},
+	template: `
+	
+	
+	
+		
+			
+				
+					[选择对象] 
+					{{def.name}} 
+				 
+			
+			
+				 
+			
+			
+				 
+			
+			
+				 
+			
+			
+		
+		
+	
+	
+		+ 
+	
+
 `
+})
+
+Vue.component("http-web-root-box", {
+	props: ["v-root-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let rootConfig = this.vRootConfig
+		if (rootConfig == null) {
+			rootConfig = {
+				isPrior: false,
+				isOn: true,
+				dir: "",
+				indexes: [],
+				stripPrefix: "",
+				decodePath: false,
+				isBreak: false
+			}
+		}
+		if (rootConfig.indexes == null) {
+			rootConfig.indexes = []
+		}
+		return {
+			rootConfig: rootConfig,
+			advancedVisible: false
+		}
+	},
+	methods: {
+		changeAdvancedVisible: function (v) {
+			this.advancedVisible = v
+		},
+		addIndex: function () {
+			let that = this
+			teaweb.popup("/servers/server/settings/web/createIndex", {
+				height: "10em",
+				callback: function (resp) {
+					that.rootConfig.indexes.push(resp.data.index)
+				}
+			})
+		},
+		removeIndex: function (i) {
+			this.rootConfig.indexes.$remove(i)
+		},
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.rootConfig.isPrior) && this.rootConfig.isOn
+		}
+	},
+	template: ``
+})
+
+Vue.component("http-webp-config-box", {
+	props: ["v-webp-config", "v-is-location", "v-is-group"],
+	data: function () {
+		let config = this.vWebpConfig
+		if (config == null) {
+			config = {
+				isPrior: false,
+				isOn: false,
+				quality: 50,
+				minLength: {count: 0, "unit": "kb"},
+				maxLength: {count: 0, "unit": "kb"},
+				mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico", "image/gif"],
+				extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"],
+				conds: null
+			}
+		}
+
+		if (config.mimeTypes == null) {
+			config.mimeTypes = []
+		}
+		if (config.extensions == null) {
+			config.extensions = []
+		}
+
+		return {
+			config: config,
+			moreOptionsVisible: false,
+			quality: config.quality
+		}
+	},
+	watch: {
+		quality: function (v) {
+			let quality = parseInt(v)
+			if (isNaN(quality)) {
+				quality = 90
+			} else if (quality < 1) {
+				quality = 1
+			} else if (quality > 100) {
+				quality = 100
+			}
+			this.config.quality = quality
+		}
+	},
+	methods: {
+		isOn: function () {
+			return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
+		},
+		changeExtensions: function (values) {
+			values.forEach(function (v, k) {
+				if (v.length > 0 && v[0] != ".") {
+					values[k] = "." + v
+				}
+			})
+			this.config.extensions = values
+		},
+		changeMimeTypes: function (values) {
+			this.config.mimeTypes = values
+		},
+		changeAdvancedVisible: function () {
+			this.moreOptionsVisible = !this.moreOptionsVisible
+		},
+		changeConds: function (conds) {
+			this.config.conds = conds
+		}
+	},
+	template: ``
+})
+
+Vue.component("origin-scheduling-view-box", {
+	props: ["v-scheduling", "v-params"],
+	data: function () {
+		let scheduling = this.vScheduling
+		if (scheduling == null) {
+			scheduling = {}
+		}
+		return {
+			scheduling: scheduling
+		}
+	},
+	methods: {
+		update: function () {
+			teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, {
+				height: "21em",
+				callback: function () {
+					window.location.reload()
+				},
+			})
+		}
+	},
+	template: `
+	
+	
+		
+			当前正在使用的算法 
+			
+				{{scheduling.name}}   [修改]  
+				
+			 
+		 
+	
+
 `
+})
+
+Vue.component("http-firewall-block-options", {
+	props: ["v-block-options"],
+	data: function () {
+		return {
+			blockOptions: this.vBlockOptions,
+			statusCode: this.vBlockOptions.statusCode,
+			timeout: this.vBlockOptions.timeout
+		}
+	},
+	watch: {
+		statusCode: function (v) {
+			let statusCode = parseInt(v)
+			if (isNaN(statusCode)) {
+				this.blockOptions.statusCode = 403
+			} else {
+				this.blockOptions.statusCode = statusCode
+			}
+		},
+		timeout: function (v) {
+			let timeout = parseInt(v)
+			if (isNaN(timeout)) {
+				this.blockOptions.timeout = 0
+			} else {
+				this.blockOptions.timeout = timeout
+			}
+		}
+	},
+	template: `	
+`
+})
+
+Vue.component("http-firewall-rules-box", {
+	props: ["v-rules", "v-type"],
+	data: function () {
+		let rules = this.vRules
+		if (rules == null) {
+			rules = []
+		}
+		return {
+			rules: rules
+		}
+	},
+	methods: {
+		addRule: function () {
+			window.UPDATING_RULE = null
+			let that = this
+			teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
+				callback: function (resp) {
+					that.rules.push(resp.data.rule)
+				}
+			})
+		},
+		updateRule: function (index, rule) {
+			window.UPDATING_RULE = rule
+			let that = this
+			teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
+				callback: function (resp) {
+					Vue.set(that.rules, index, resp.data.rule)
+				}
+			})
+		},
+		removeRule: function (index) {
+			let that = this
+			teaweb.confirm("确定要删除此规则吗?", function () {
+				that.rules.$remove(index)
+			})
+		}
+	},
+	template: `
+		
+		
+			
+				{{rule.name}}[{{rule.param}}] 
+				
+				
+				
+					{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
+				 	
+				
+				
+				
+					{{rule.checkpointOptions.allowDomains}}
+				 
+				
+				
+					 | {{paramFilter.code}}  {{rule.operator}}  {{rule.value}}
+				 
+				
+				
+				
+			
+			
+		
+		
+ 
+
 `
+})
+
+Vue.component("http-fastcgi-box", {
+	props: ["v-fastcgi-ref", "v-fastcgi-configs", "v-is-location"],
+	data: function () {
+		let fastcgiRef = this.vFastcgiRef
+		if (fastcgiRef == null) {
+			fastcgiRef = {
+				isPrior: false,
+				isOn: false,
+				fastcgiIds: []
+			}
+		}
+		let fastcgiConfigs = this.vFastcgiConfigs
+		if (fastcgiConfigs == null) {
+			fastcgiConfigs = []
+		} else {
+			fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) {
+				return v.id
+			})
+		}
+
+		return {
+			fastcgiRef: fastcgiRef,
+			fastcgiConfigs: fastcgiConfigs,
+			advancedVisible: false
+		}
+	},
+	methods: {
+		isOn: function () {
+			return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn
+		},
+		createFastcgi: function () {
+			let that = this
+			teaweb.popup("/servers/server/settings/fastcgi/createPopup", {
+				height: "26em",
+				callback: function (resp) {
+					teaweb.success("添加成功", function () {
+						that.fastcgiConfigs.push(resp.data.fastcgi)
+						that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id)
+					})
+				}
+			})
+		},
+		updateFastcgi: function (fastcgiId, index) {
+			let that = this
+			teaweb.popup("/servers/server/settings/fastcgi/updatePopup?fastcgiId=" + fastcgiId, {
+				callback: function (resp) {
+					teaweb.success("修改成功", function () {
+						Vue.set(that.fastcgiConfigs, index, resp.data.fastcgi)
+					})
+				}
+			})
+		},
+		removeFastcgi: function (index) {
+			this.fastcgiRef.fastcgiIds.$remove(index)
+			this.fastcgiConfigs.$remove(index)
+		}
+	},
+	template: ``
+})
+
+// URL扩展名条件
+Vue.component("http-cond-url-extension", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPathExtension}",
+			operator: "in",
+			value: "[]"
+		}
+		if (this.vCond != null && this.vCond.param == cond.param) {
+			cond.value = this.vCond.value
+		}
+
+		let extensions = []
+		try {
+			extensions = JSON.parse(cond.value)
+		} catch (e) {
+
+		}
+
+		return {
+			cond: cond,
+			extensions: extensions, // TODO 可以拖动排序
+
+			isAdding: false,
+			addingExt: ""
+		}
+	},
+	watch: {
+		extensions: function () {
+			this.cond.value = JSON.stringify(this.extensions)
+		}
+	},
+	methods: {
+		addExt: function () {
+			this.isAdding = !this.isAdding
+
+			if (this.isAdding) {
+				let that = this
+				setTimeout(function () {
+					that.$refs.addingExt.focus()
+				}, 100)
+			}
+		},
+		cancelAdding: function () {
+			this.isAdding = false
+			this.addingExt = ""
+		},
+		confirmAdding: function () {
+			// TODO 做更详细的校验
+			// TODO 如果有重复的则提示之
+
+			if (this.addingExt.length == 0) {
+				return
+			}
+			if (this.addingExt[0] != ".") {
+				this.addingExt = "." + this.addingExt
+			}
+			this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase()
+			this.extensions.push(this.addingExt)
+
+			// 清除状态
+			this.cancelAdding()
+		},
+		removeExt: function (index) {
+			this.extensions.$remove(index)
+		}
+	},
+	template: ``
+})
+
+// 根据URL前缀
+Vue.component("http-cond-url-prefix", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "prefix",
+			value: ""
+		}
+		if (this.vCond != null && typeof (this.vCond.value) == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+Vue.component("http-cond-url-not-prefix", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "prefix",
+			value: "",
+			isReverse: true
+		}
+		if (this.vCond != null && typeof this.vCond.value == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+// URL精准匹配
+Vue.component("http-cond-url-eq", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "eq",
+			value: ""
+		}
+		if (this.vCond != null && typeof this.vCond.value == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+Vue.component("http-cond-url-not-eq", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "eq",
+			value: "",
+			isReverse: true
+		}
+		if (this.vCond != null && typeof this.vCond.value == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+// URL正则匹配
+Vue.component("http-cond-url-regexp", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "regexp",
+			value: ""
+		}
+		if (this.vCond != null && typeof this.vCond.value == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+// 排除URL正则匹配
+Vue.component("http-cond-url-not-regexp", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "${requestPath}",
+			operator: "not regexp",
+			value: ""
+		}
+		if (this.vCond != null && typeof this.vCond.value == "string") {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond
+		}
+	},
+	template: `
+	 
+	 
+	
+
`
+})
+
+// 根据MimeType
+Vue.component("http-cond-mime-type", {
+	props: ["v-cond"],
+	data: function () {
+		let cond = {
+			isRequest: false,
+			param: "${response.contentType}",
+			operator: "mime type",
+			value: "[]"
+		}
+		if (this.vCond != null && this.vCond.param == cond.param) {
+			cond.value = this.vCond.value
+		}
+		return {
+			cond: cond,
+			mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序
+
+			isAdding: false,
+			addingMimeType: ""
+		}
+	},
+	watch: {
+		mimeTypes: function () {
+			this.cond.value = JSON.stringify(this.mimeTypes)
+		}
+	},
+	methods: {
+		addMimeType: function () {
+			this.isAdding = !this.isAdding
+
+			if (this.isAdding) {
+				let that = this
+				setTimeout(function () {
+					that.$refs.addingMimeType.focus()
+				}, 100)
+			}
+		},
+		cancelAdding: function () {
+			this.isAdding = false
+			this.addingMimeType = ""
+		},
+		confirmAdding: function () {
+			// TODO 做更详细的校验
+			// TODO 如果有重复的则提示之
+
+			if (this.addingMimeType.length == 0) {
+				return
+			}
+			this.addingMimeType = this.addingMimeType.replace(/\s+/g, "")
+			this.mimeTypes.push(this.addingMimeType)
+
+			// 清除状态
+			this.cancelAdding()
+		},
+		removeMimeType: function (index) {
+			this.mimeTypes.$remove(index)
+		}
+	},
+	template: `
+	
+	
+	
+	
+		+添加MimeType 
+	
+	
+
 `
+})
+
+// 参数匹配
+Vue.component("http-cond-params", {
+	props: ["v-cond"],
+	mounted: function () {
+		let cond = this.vCond
+		if (cond == null) {
+			return
+		}
+		this.operator = cond.operator
+
+		// stringValue
+		if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) {
+			this.stringValue = cond.value
+			return
+		}
+
+		// numberValue
+		if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) {
+			this.numberValue = cond.value
+			return
+		}
+
+		// modValue
+		if (["mod", "ip mod"].$contains(cond.operator)) {
+			let pieces = cond.value.split(",")
+			this.modDivValue = pieces[0]
+			if (pieces.length > 1) {
+				this.modRemValue = pieces[1]
+			}
+			return
+		}
+
+		// stringValues
+		let that = this
+		if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) {
+			try {
+				let arr = JSON.parse(cond.value)
+				if (arr != null && (arr instanceof Array)) {
+					arr.forEach(function (v) {
+						that.stringValues.push(v)
+					})
+				}
+			} catch (e) {
+
+			}
+			return
+		}
+
+		// versionValue
+		if (["version range"].$contains(cond.operator)) {
+			let pieces = cond.value.split(",")
+			this.versionRangeMinValue = pieces[0]
+			if (pieces.length > 1) {
+				this.versionRangeMaxValue = pieces[1]
+			}
+			return
+		}
+	},
+	data: function () {
+		let cond = {
+			isRequest: true,
+			param: "",
+			operator: window.REQUEST_COND_OPERATORS[0].op,
+			value: ""
+		}
+		if (this.vCond != null) {
+			cond = this.vCond
+		}
+		return {
+			cond: cond,
+			operators: window.REQUEST_COND_OPERATORS,
+			operator: window.REQUEST_COND_OPERATORS[0].op,
+			operatorDescription: window.REQUEST_COND_OPERATORS[0].description,
+			variables: window.REQUEST_VARIABLES,
+			variable: "",
+
+			// 各种类型的值
+			stringValue: "",
+			numberValue: "",
+
+			modDivValue: "",
+			modRemValue: "",
+
+			stringValues: [],
+
+			versionRangeMinValue: "",
+			versionRangeMaxValue: ""
+		}
+	},
+	methods: {
+		changeVariable: function () {
+			let v = this.cond.param
+			if (v == null) {
+				v = ""
+			}
+			this.cond.param = v + this.variable
+		},
+		changeOperator: function () {
+			let that = this
+			this.operators.forEach(function (v) {
+				if (v.op == that.operator) {
+					that.operatorDescription = v.description
+				}
+			})
+
+			this.cond.operator = this.operator
+
+			// 移动光标
+			let box = document.getElementById("variables-value-box")
+			if (box != null) {
+				setTimeout(function () {
+					let input = box.getElementsByTagName("INPUT")
+					if (input.length > 0) {
+						input[0].focus()
+					}
+				}, 100)
+			}
+		},
+		changeStringValues: function (v) {
+			this.stringValues = v
+			this.cond.value = JSON.stringify(v)
+		}
+	},
+	watch: {
+		stringValue: function (v) {
+			this.cond.value = v
+		},
+		numberValue: function (v) {
+			// TODO 校验数字
+			this.cond.value = v
+		},
+		modDivValue: function (v) {
+			if (v.length == 0) {
+				return
+			}
+			let div = parseInt(v)
+			if (isNaN(div)) {
+				div = 1
+			}
+			this.modDivValue = div
+			this.cond.value = div + "," + this.modRemValue
+		},
+		modRemValue: function (v) {
+			if (v.length == 0) {
+				return
+			}
+			let rem = parseInt(v)
+			if (isNaN(rem)) {
+				rem = 0
+			}
+			this.modRemValue = rem
+			this.cond.value = this.modDivValue + "," + rem
+		},
+		versionRangeMinValue: function (v) {
+			this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
+		},
+		versionRangeMaxValue: function (v) {
+			this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
+		}
+	},
+	template: `
+	
+		参数值 
+		
+			 
+			
+				
+					
+						 
+					
+					
+						
+							[常用参数] 
+							{{v.code}} - {{v.name}} 
+						 
+					
+				
			
+			
 
+			
+		 
+	 
+	
+		操作符 
+		
+			
+				
+					{{operator.name}} 
+				 
+				
+			
+		 
+	 
+	
+		对比值 
+		
+			
+			
+				 
+				
+			
+			
+			
+			
+				 
+				
+			
+			
+			
+			
+				 
+				
+			
+			
+				 
+				
+			
+			
+			
+			
+			
+				 
+				
+				
+				
+				
+				
+				
+			
+			
+				 
+				
+				
+				
+				
+			
+			
+			
+			
+			
+				 
+				
+			
+			
+				 
+				
+			
+			
+				 
+				
+			
+		 
+	 
+ `
+})
+
+Vue.component("server-group-selector", {
+	props: ["v-groups"],
+	data: function () {
+		let groups = this.vGroups
+		if (groups == null) {
+			groups = []
+		}
+		return {
+			groups: groups
+		}
+	},
+	methods: {
+		selectGroup: function () {
+			let that = this
+			let groupIds = this.groups.map(function (v) {
+				return v.id.toString()
+			}).join(",")
+			teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, {
+				callback: function (resp) {
+					that.groups.push(resp.data.group)
+				}
+			})
+		},
+		addGroup: function () {
+			let that = this
+			teaweb.popup("/servers/groups/createPopup", {
+				callback: function (resp) {
+					that.groups.push(resp.data.group)
+				}
+			})
+		},
+		removeGroup: function (index) {
+			this.groups.$remove(index)
+		},
+		groupIds: function () {
+			return this.groups.map(function (v) {
+				return v.id
+			})
+		}
+	},
+	template: ``
+})
+
+// 指标周期设置
+Vue.component("metric-period-config-box", {
+	props: ["v-period", "v-period-unit"],
+	data: function () {
+		let period = this.vPeriod
+		let periodUnit = this.vPeriodUnit
+		if (period == null || period.toString().length == 0) {
+			period = 1
+		}
+		if (periodUnit == null || periodUnit.length == 0) {
+			periodUnit = "day"
+		}
+		return {
+			periodConfig: {
+				period: period,
+				unit: periodUnit
+			}
+		}
+	},
+	watch: {
+		"periodConfig.period": function (v) {
+			v = parseInt(v)
+			if (isNaN(v) || v <= 0) {
+				v = 1
+			}
+			this.periodConfig.period = v
+		}
+	},
+	template: `
+	
+	
+		
+			 
+		
+		
+			
+				分钟 
+				小时 
+				天 
+				周 
+				月 
+			 
+		
+	
+	
+
 `
+})
+
+Vue.component("traffic-limit-config-box", {
+	props: ["v-traffic-limit"],
+	data: function () {
+		let config = this.vTrafficLimit
+		if (config == null) {
+			config = {
+				isOn: false,
+				dailySize: {
+					count: -1,
+					unit: "gb"
+				},
+				monthlySize: {
+					count: -1,
+					unit: "gb"
+				},
+				totalSize: {
+					count: -1,
+					unit: "gb"
+				},
+				noticePageBody: ""
+			}
+		}
+		if (config.dailySize == null) {
+			config.dailySize = {
+				count: -1,
+				unit: "gb"
+			}
+		}
+		if (config.monthlySize == null) {
+			config.monthlySize = {
+				count: -1,
+				unit: "gb"
+			}
+		}
+		if (config.totalSize == null) {
+			config.totalSize = {
+				count: -1,
+				unit: "gb"
+			}
+		}
+		return {
+			config: config
+		}
+	},
+	methods: {
+		showBodyTemplate: function () {
+			this.config.noticePageBody = `
+
+
+Traffic Limit Exceeded Warning 
+
+
+The site traffic has exceeded the limit. Please contact with the site administrator.
+
+
+`
+		}
+	},
+	template: `
+	
+	
+		
+			
+				是否启用 
+				
+					 
+					
+				 
+			 
+		 
+		
+			
+				日流量限制 
+				
+					 
+				 
+			 
+			
+				月流量限制 
+				
+					 
+				 
+			 
+			
+			
+				网页提示内容 
+				
+					
+					
+				 
+			 
+		 
+	
+	
+
 `
+})
+
+// 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: `
+    
+        [选择系统用户] 
+        {{admin.name}}({{admin.username}}) 
+     
+
`
+})
+
+// 绑定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: ``
+})
+
+Vue.component("ip-list-table", {
+	props: ["v-items", "v-keyword", "v-show-search-button"],
+	data: function () {
+		return {
+			items: this.vItems,
+			keyword: (this.vKeyword != null) ? this.vKeyword : "",
+			selectedAll: false,
+			hasSelectedItems: false
+		}
+	},
+	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)
+				})
+		}
+	},
+	template: ``
+})
+
+Vue.component("ip-item-text", {
+    props: ["v-item"],
+    template: `
+    * 
+    
+        {{vItem.ipFrom}}
+        - {{vItem.ipTo}} 
+     
+    {{vItem.ipFrom}} 
+      级别:{{vItem.eventLevelName}} 
+ `
+})
+
+Vue.component("ip-box", {
+	props: [],
+	methods: {
+		popup: function () {
+			let e = this.$refs.container
+			let text = e.innerText
+			if (text == null) {
+				text = e.textContent
+			}
+
+			teaweb.popup("/servers/ipbox?ip=" + text, {
+				width: "50em",
+				height: "30em"
+			})
+		}
+	},
+	template: ` `
+})
+
+Vue.component("api-node-selector", {
+	props: [],
+	data: function () {
+		return {}
+	},
+	template: `
+	暂未实现
+
`
+})
+
+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("/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("/api/node/updateAddrPopup?addressId=", {
+				callback: function (resp) {
+					Vue.set(that.addrs, index, resp.data.addr);
+				}
+			})
+		},
+
+		// 删除IP地址
+		removeAddr: function (index) {
+			this.addrs.$remove(index);
+		}
+	},
+	template: `
+	
+	
+		
+			
+				{{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}}
+				
+				
+			
+		
+		
+	
+	
+		+ 
+	
+
 `
+})
+
+// 给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)
+}
+
+
+Vue.component("page-box", {
+	data: function () {
+		return {
+			page: ""
+		}
+	},
+	created: function () {
+		let that = this;
+		setTimeout(function () {
+			that.page = Tea.Vue.page;
+		})
+	},
+	template: ``
+})
+
+Vue.component("network-addresses-box", {
+	props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range"],
+	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
+		}
+	},
+	watch: {
+		"vServerType": function () {
+			this.addresses = []
+		},
+		"vAddresses": function () {
+			if (this.vAddresses != null) {
+				this.addresses = this.vAddresses
+			}
+		}
+	},
+	methods: {
+		addAddr: function () {
+			let that = this
+			window.UPDATING_ADDR = null
+			teaweb.popup("/servers/addPortPopup?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
+			teaweb.popup("/servers/addPortPopup?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")
+		}
+	},
+	template: `
+	
+	
+		
+			{{addr.protocol}}://
{{addr.host.quoteIP()}} * :
{{addr.portRange}} {{addr.portRange}} 
+			
+			
  
+		
+	
+	
[添加端口绑定] 
+
 `
+})
+
+/**
+ * 保存按钮
+ */
+Vue.component("submit-btn", {
+	template: '保存  '
+});
+
+// 可以展示更多条目的角图表
+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 = ""
+					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: ` `
+})
+
+/**
+ * 菜单项
+ */
+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: '\
+		  \
+		'
+});
+
+// 使用Icon的链接方式
+Vue.component("link-icon", {
+	props: ["href", "title", "target"],
+	data: function () {
+		return {
+			vTitle: (this.title == null) ? "打开链接" : this.title
+		}
+	},
+	template: `   `
+})
+
+// 带有下划虚线的连接
+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)
+		}
+	},
+	template: ` `
+})
+
+// 会弹出窗口的链接
+Vue.component("link-popup", {
+	props: ["title"],
+	methods: {
+		clickPrevent: function () {
+			emitClick(this, arguments)
+		}
+	},
+	template: ` `
+})
+
+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: `   `
+})
+
+// 小提示
+Vue.component("tip-icon", {
+	props: ["content"],
+	methods: {
+		showTip: function () {
+			teaweb.popupTip(this.content)
+		}
+	},
+	template: ` `
+})
+
+// 提交点击事件
+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)
+}
+
+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: ``
+})
+
+Vue.component("more-options-tbody", {
+	data: function () {
+		return {
+			isVisible: false
+		}
+	},
+	methods: {
+		show: function () {
+			this.isVisible = !this.isVisible
+			this.$emit("change", this.isVisible)
+		}
+	},
+	template: `
+	
+		更多选项 收起选项  
+	 
+ `
+})
+
+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) {
+					teaweb.warn("找不到要下载的内容")
+					return
+				}
+				text = e.innerText
+				if (text == null) {
+					text = e.textContent
+				}
+			}
+			return Tea.url("/ui/download", {
+				file: this.file,
+				text: text
+			})
+		}
+	},
+	template: ` `,
+})
+
+Vue.component("values-box", {
+	props: ["values", "size", "maxlength", "name", "placeholder"],
+	data: function () {
+		let values = this.values;
+		if (values == null) {
+			values = [];
+		}
+		return {
+			"vValues": 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.vValues[index];
+			var that = this;
+			setTimeout(function () {
+				that.$refs.value.focus();
+			}, 200);
+		},
+		confirm: function () {
+			if (this.value.length == 0) {
+				return
+			}
+
+			if (this.isUpdating) {
+				Vue.set(this.vValues, this.index, this.value);
+			} else {
+				this.vValues.push(this.value);
+			}
+			this.cancel()
+			this.$emit("change", this.vValues)
+		},
+		remove: function (index) {
+			this.vValues.$remove(index)
+			this.$emit("change", this.vValues)
+		},
+		cancel: function () {
+			this.isUpdating = false;
+			this.isAdding = false;
+			this.value = "";
+		},
+		updateAll: function (values) {
+			this.vValeus = values
+		},
+		addValue: function (v) {
+			this.vValues.push(v)
+		},
+
+		startEditing: function () {
+			this.isEditing = !this.isEditing
+		}
+	},
+	template: ``
+});
+
+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 () {
+			let date = new Date()
+
+			// 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])
+			date.setFullYear(year)
+
+			let month = parseInt(pieces[1])
+			if (month < 1 || month > 12) {
+				this.hasDayError = true
+				return
+			}
+			date.setMonth(month - 1)
+
+			let day = parseInt(pieces[2])
+			if (day < 1 || day > 32) {
+				this.hasDayError = true
+				return
+			}
+			date.setDate(day)
+
+			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
+			date.setHours(hour)
+
+			// 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
+			date.setMinutes(minute)
+
+			// 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
+			date.setSeconds(second)
+
+			this.timestamp = Math.floor(date.getTime() / 1000)
+		},
+		leadingZero: function (s, l) {
+			if (l <= s.length) {
+				return s
+			}
+			for (let i = 0; i < l - s.length; i++) {
+				s = "0" + s
+			}
+			return s
+		}
+	},
+	template: ``
+})
+
+// 启用状态标签
+Vue.component("label-on", {
+	props: ["v-is-on"],
+	template: '已启用 已停用 
'
+})
+
+// 文字代码标签
+Vue.component("code-label", {
+	methods: {
+		click: function (args) {
+			this.$emit("click", args)
+		}
+	},
+	template: ` `
+})
+
+// tiny标签
+Vue.component("tiny-label", {
+	template: ` `
+})
+
+Vue.component("tiny-basic-label", {
+	template: ` `
+})
+
+// 更小的标签
+Vue.component("micro-basic-label", {
+	template: ` `
+})
+
+
+// 灰色的Label
+Vue.component("grey-label", {
+	template: ` `
+})
+
+
+/**
+ * 一级菜单
+ */
+Vue.component("first-menu", {
+	props: [],
+	template: ' \
+		'
+});
+
+/**
+ * 更多选项
+ */
+Vue.component("more-options-indicator", {
+	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)
+		}
+	},
+	template: '更多选项 收起选项      '
+});
+
+/**
+ * 二级菜单
+ */
+Vue.component("second-menu", {
+	template: ' \
+		'
+});
+
+Vue.component("more-options-angle", {
+	data: function () {
+		return {
+			isVisible: false
+		}
+	},
+	methods: {
+		show: function () {
+			this.isVisible = !this.isVisible
+			this.$emit("change", this.isVisible)
+		}
+	},
+	template: `更多选项 收起选项  `
+})
+
+/**
+ * 菜单项
+ */
+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: '\
+		[ ]  \
+		'
+});
+
+Vue.component("health-check-config-box", {
+	props: ["v-health-check-config"],
+	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: false
+			}
+			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
+		}
+	},
+	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()
+		},
+		"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
+				}
+			})
+		},
+		editURL: function () {
+			this.urlIsEditing = !this.urlIsEditing
+		}
+	},
+	template: ``
+})
+
+Vue.component("time-duration-box", {
+	props: ["v-name", "v-value", "v-count", "v-unit"],
+	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
+		}
+		return {
+			duration: v,
+			countString: (v.count >= 0) ? v.count.toString() : ""
+		}
+	},
+	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: `
+	
+	
+		 
+	
+	
+		
+			毫秒 
+			秒 
+			分钟 
+			小时 
+			天 
+		 
+	
+
 `
+})
+
+Vue.component("not-found-box", {
+	props: ["message"],
+	template: ``
+})
+
+// 警告消息
+Vue.component("warning-message", {
+	template: ``
+})
+
+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)
+		}
+	},
+    watch: {
+	    value: function (v) {
+	        if (typeof v == "boolean") {
+	            this.newValue = v
+            }
+        }
+    },
+	template: `
+	 
+	 
+
`
+})
+
+Vue.component("network-addresses-view", {
+	props: ["v-addresses"],
+	template: `
+	
+		{{addr.protocol}}://{{addr.host.quoteIP()}} * :{{addr.portRange}}
+	
+
 `
+})
+
+Vue.component("size-capacity-view", {
+	props:["v-default-text", "v-value"],
+	template: `
+	{{vValue.count}}{{vValue.unit.toUpperCase()}} 
+	{{vDefaultText}} 
+
`
+})
+
+// 信息提示窗口
+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: ``
+})
+
+Vue.component("keyword", {
+	props: ["v-word"],
+	data: function () {
+		let word = this.vWord
+		if (word == null) {
+			word = ""
+		} else {
+			word = word.replace(/\)/, "\\)")
+			word = word.replace(/\(/, "\\(")
+			word = word.replace(/\+/, "\\+")
+			word = word.replace(/\^/, "\\^")
+			word = word.replace(/\$/, "\\$")
+		}
+
+		let slot = this.$slots["default"][0]
+		let text = this.encodeHTML(slot.text)
+		if (word.length > 0) {
+			text = text.replace(new RegExp("(" + word + ")", "ig"), "$1 ")
+		}
+
+		return {
+			word: word,
+			text: text
+		}
+	},
+	methods: {
+		encodeHTML: function (s) {
+			s = s.replace("&", "&")
+			s = s.replace("<", "<")
+			s = s.replace(">", ">")
+			return s
+		}
+	},
+	template: ` `
+})
+
+Vue.component("node-log-row", {
+	props: ["v-log", "v-keyword"],
+	data: function () {
+		return {
+			log: this.vLog,
+			keyword: this.vKeyword
+		}
+	},
+	template: `
+	
[{{log.createdTime}}] [{{log.createdTime}}] [{{log.tag}}]{{log.description}}     共{{log.count}}条  {{log.server.name}} 
+
 `
+})
+
+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: ``
+})
+
+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: ` `
+})
+
+
+Vue.component("labeled-input", {
+	props: ["name", "size", "maxlength", "label", "value"],
+	template: ' \
+	 \
+	{{label}} \
+
'
+});
+
+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: `
+	 
+	 
+
`
+})
+
+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: ` `
+})
+
+// 节点角色名称
+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: `{{roleName}} `
+})
+
+let sourceCodeBoxIndex = 0
+
+Vue.component("source-code-box", {
+	props: ["name", "type", "id", "read-only"],
+	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
+		}
+		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
+		})
+		boxEditor.setValue(value)
+
+		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)
+		}
+	},
+	data: function () {
+		let index = sourceCodeBoxIndex++
+
+		let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex
+		if (this.id != null) {
+			valueBoxId = this.id
+		}
+
+		return {
+			index: index,
+			valueBoxId: valueBoxId
+		}
+	},
+	template: ``
+})
+
+Vue.component("size-capacity-box", {
+	props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"],
+	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
+		}
+
+		return {
+			capacity: v,
+			countString: (v.count >= 0) ? v.count.toString() : "",
+			vSize: vSize,
+			vMaxlength: vMaxlength
+		}
+	},
+	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: `
+	
+	
+		 
+	
+	
+		
+			字节 
+			KB 
+			MB 
+			GB 
+			TB 
+			PB 
+		 
+	
+
 `
+})
+
+/**
+ * 二级菜单
+ */
+Vue.component("inner-menu", {
+	template: `
+		`
+});
+
+Vue.component("datepicker", {
+	props: ["v-name", "v-value", "v-bottom-left"],
+	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 = "day"
+		}
+
+		let day = this.vValue
+		if (day == null) {
+			day = ""
+		}
+
+		return {
+			name: name,
+			day: day
+		}
+	},
+	methods: {
+		change: function () {
+			this.$emit("change", this.day)
+		}
+	},
+	template: `
+	 
+
`
+})
+
+// 排序使用的箭头
+Vue.component("sort-arrow", {
+	props: ["name"],
+	data: function () {
+		let url = window.location.toString()
+		let order = ""
+		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 (argValue != "asc" && argValue != "desc") {
+						newArgs.push(v)
+					}
+				} else {
+					newArgs.push(v)
+				}
+			})
+		}
+		if (order == "asc") {
+			newArgs.push(this.name + "=desc")
+		} else if (order == "desc") {
+			newArgs.push(this.name + "=asc")
+		} else {
+			newArgs.push(this.name + "=desc")
+		}
+
+		let qIndex = url.indexOf("?")
+		if (qIndex > 0) {
+			url = url.substring(0, qIndex) + "?" + newArgs.join("&")
+		} else {
+			url = url + "?" + newArgs.join("&")
+		}
+
+		return {
+			order: order,
+			url: url
+		}
+	},
+	template: `   `
+})
+
+Vue.component("user-link", {
+	props: ["v-user", "v-keyword"],
+	data: function () {
+		let user = this.vUser
+		if (user == null) {
+			user = {id: 0, "username": "", "fullname": ""}
+		}
+		return {
+			user: user
+		}
+	},
+	template: `
+	{{user.fullname}} ({{user.username}} )  
+	[已删除] 
+
`
+})
+
+// 监控节点分组选择
+Vue.component("report-node-groups-selector", {
+	props: ["v-group-ids"],
+	mounted: function () {
+		let that = this
+		Tea.action("/clusters/monitors/groups/options")
+			.post()
+			.success(function (resp) {
+				that.groups = resp.data.groups.map(function (group) {
+					group.isChecked = that.groupIds.$contains(group.id)
+					return group
+				})
+				that.isLoaded = true
+			})
+	},
+	data: function () {
+		var groupIds = this.vGroupIds
+		if (groupIds == null) {
+			groupIds = []
+		}
+
+		return {
+			groups: [],
+			groupIds: groupIds,
+			isLoaded: false,
+			allGroups: groupIds.length == 0
+		}
+	},
+	methods: {
+		check: function (group) {
+			group.isChecked = !group.isChecked
+			this.groupIds = []
+			let that = this
+			this.groups.forEach(function (v) {
+				if (v.isChecked) {
+					that.groupIds.push(v.id)
+				}
+			})
+			this.change()
+		},
+		change: function () {
+			let that = this
+			let groups = []
+			this.groupIds.forEach(function (groupId) {
+				let group = that.groups.$find(function (k, v) {
+					return v.id == groupId
+				})
+				if (group == null) {
+					return
+				}
+				groups.push({
+					id: group.id,
+					name: group.name
+				})
+			})
+			this.$emit("change", groups)
+		}
+	},
+	watch: {
+		allGroups: function (b) {
+			if (b) {
+				this.groupIds = []
+				this.groups.forEach(function (v) {
+					v.isChecked = false
+				})
+			}
+
+			this.change()
+		}
+	},
+	template: ``
+})
+
+Vue.component("finance-user-selector", {
+	mounted: function () {
+		let that = this
+
+		Tea.action("/finance/users/options")
+			.post()
+			.success(function (resp) {
+				that.users = resp.data.users
+			})
+	},
+	props: ["v-user-id"],
+	data: function () {
+		let userId = this.vUserId
+		if (userId == null) {
+			userId = 0
+		}
+		return {
+			users: [],
+			userId: userId
+		}
+	},
+	watch: {
+		userId: function (v) {
+			this.$emit("change", v)
+		}
+	},
+	template: `
+	
+		[选择用户] 
+		{{user.fullname}} ({{user.username}}) 
+	 
+
`
+})
+
+// 节点登录推荐端口
+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: `
+	正在检查端口... 
+	
+		可能端口:{{port}} 
+		   
+	 
+	
+		常用端口:{{port}} 
+	 
+	常用端口有22等。 
+	(可以点击要使用的端口) 
+ `
+})
+
+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: `
+	
+		
+		{{selectedGroup.name}}  
+	
+	
+
 `
+})
+
+// 节点IP地址管理(标签形式)
+Vue.component("node-ip-addresses-box", {
+	props: ["v-ip-addresses", "role"],
+	data: function () {
+		return {
+			ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
+			supportThresholds: this.role != "ns"
+		}
+	},
+	methods: {
+		// 添加IP地址
+		addIPAddress: function () {
+			window.UPDATING_NODE_IP_ADDRESS = null
+
+			let that = this;
+			teaweb.popup("/nodes/ipAddresses/createPopup?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 = address
+
+			let that = this;
+			teaweb.popup("/nodes/ipAddresses/updatePopup?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: `
+	
+	
+		
+			
+				
[IPv6]  {{address.ip}}
+				
({{address.name}},不可访问 ) 
+				
(不可访问) 
+				
[off] 
+				
[down] 
+				
[{{address.thresholds.length}}个阈值] 
+				 
+				
+				
+			
+		
+		
+	
+	
+		+ 
+	
+
 `
+})
+
+// 节点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: `
+	
+	
+		
+			
+				
+					
+						[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
+					 	 
+					{{itemName(item.item)}}
+					
+					
+						
+						成功 
+						失败 
+					 
+					
+						
+						[{{group.name}}     ] 
+						
+						 [{{itemOperatorName(item.operator)}}]  {{item.value}}{{itemUnitName(item.item)}}  
+					  
+				  
+				 AND    
+				->
+				{{actionName(action.action)}}
+				到{{action.options.ips.join(", ")}} 
+				({{action.options.url}}) 
+				  					 
+				 AND   
+			  
+		
+	
+
 `
+})
+
+// 节点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: `
+	
+		
+	
+	
+		
+			
+				
+					[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
+				  
+				{{itemName(item.item)}}
+				
+				
+					
+					成功 
+					失败 
+				 
+				
+					
+					[{{group.name}}     ] 
+				
+					[{{itemOperatorName(item.operator)}}]   {{item.value}}{{itemUnitName(item.item)}} 
+			 	 
+			 	 AND   
+			 
+			->
+			
{{actionName(action.action)}}
+			到{{action.options.ips.join(", ")}} 
+			({{action.options.url}}) 
+			  AND    
+			 
+			
 
+			
+		
+	
+	
+	
+	
+		
+			
+				
+					阈值 
+					动作 
+				 
+			 
+			
+				
+					
+					
+						
+							
+								[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
+							  
+							{{itemName(item.item)}}
+							
+							
+								
+								成功 
+								失败 
+							 
+							
+								
+								[{{group.name}}     ] 
+								 [{{itemOperatorName(item.operator)}}]  {{item.value}}{{itemUnitName(item.item)}}
+							   
+							  
+							
+						
+					
 
+					
+					
+					
+					
+						+ 
+					
+				 
+				
+					
+					
+						
+							{{actionName(action.action)}}  
+							
到{{action.options.ips.join(", ")}} 
+							
({{action.options.url}}) 
+							
+						
+					
 
+					
+					
+					
+					
+					
+						+ 
+					
	
+				 
+			 
+		
+		
+		
+		
+	
+	
+	
+		+ 
+	
+
 `
+})
+
+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: `
+	
+		
+		{{selectedRegion.name}}  
+	
+	
+
 `
+})
+
+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
+			})
+		}
+	},
+	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.domainName, keyword)
+			})
+			if (this.searchingRoutes.length > 0) {
+				this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId
+			} else {
+				this.routeCode = ""
+			}
+		}
+	},
+	template: `
+	
+	
+		
+			{{route.name}} ({{route.domainName}})  
+		 
+		
+	
+	
+ 
+	
+		
+			
+				
+					[请选择] 
+					{{route.name}}({{route.domainName}}) 
+				 
+			
+			
+				 
+			
+			
+			
+				确定 
+			
+			
+		
+	
+
 `
+})
+
+Vue.component("dns-domain-selector", {
+	props: ["v-domain-id", "v-domain-name"],
+	data: function () {
+		let domainId = this.vDomainId
+		if (domainId == null) {
+			domainId = 0
+		}
+		let domainName = this.vDomainName
+		if (domainName == null) {
+			domainName = ""
+		}
+		return {
+			domainId: domainId,
+			domainName: domainName
+		}
+	},
+	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.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.change()
+				}
+			})
+		},
+		change: function () {
+			this.$emit("change", {
+				id: this.domainId,
+				name: this.domainName
+			})
+		}
+	},
+	template: `
+	
+	
+		
+			{{domainName}}
+			 
+			 
+		 
+	
+	
+
 `
+})
+
+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: `
+	
+	
{{grant.name}}
({{grant.methodName}}) ({{grant.username}})      
+	
+
 `
+})
+
+window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true},{"type":"url-prefix","name":"URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL前缀","isRequest":true},{"type":"url-eq","name":"URL精准匹配","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true},{"type":"url-not-eq","name":"排除:URL精准匹配","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false}]
+
+window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"},{"description":"判断参数值解析后的文件是否存在","name":"文件存在","op":"file exist"},{"description":"判断参数值解析后的文件是否不存在","name":"文件不存在","op":"file not exist"}]
+
+window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":"边缘节点版本"},{"code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险","name":"客户端地址(IP)"},{"code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","name":"客户端地址(IP)"},{"code":"${remotePort}","description":"","name":"客户端端口"},{"code":"${remoteUser}","description":"","name":"客户端用户名"},{"code":"${requestURI}","description":"比如/hello?name=lily","name":"请求URI"},{"code":"${requestPath}","description":"比如/hello","name":"请求路径(不包括参数)"},{"code":"${requestURL}","description":"比如https://example.com/hello?name=lily","name":"完整的请求URL"},{"code":"${requestLength}","description":"","name":"请求内容长度"},{"code":"${requestMethod}","description":"比如GET、POST","name":"请求方法"},{"code":"${requestFilename}","description":"","name":"请求文件路径"},{"code":"${scheme}","description":"","name":"请求协议,http或https"},{"code":"${proto}","description:":"类似于HTTP/1.0","name":"包含版本的HTTP请求协议"},{"code":"${timeISO8601}","description":"比如2018-07-16T23:52:24.839+08:00","name":"ISO 8601格式的时间"},{"code":"${timeLocal}","description":"比如17/Jul/2018:09:52:24 +0800","name":"本地时间"},{"code":"${msec}","description":"比如1531756823.054","name":"带有毫秒的时间"},{"code":"${timestamp}","description":"","name":"unix时间戳,单位为秒"},{"code":"${host}","description":"","name":"主机名"},{"code":"${serverName}","description":"","name":"接收请求的服务器名"},{"code":"${serverPort}","description":"","name":"接收请求的服务器端口"},{"code":"${referer}","description":"","name":"请求来源URL"},{"code":"${referer.host}","description":"","name":"请求来源URL域名"},{"code":"${userAgent}","description":"","name":"客户端信息"},{"code":"${contentType}","description":"","name":"请求头部的Content-Type"},{"code":"${cookies}","description":"","name":"所有cookie组合字符串"},{"code":"${cookie.NAME}","description":"","name":"单个cookie值"},{"code":"${args}","description":"","name":"所有参数组合字符串"},{"code":"${arg.NAME}","description":"","name":"单个参数值"},{"code":"${headers}","description":"","name":"所有Header信息组合字符串"},{"code":"${header.NAME}","description":"","name":"单个Header值"}]
+
+window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"请求协议Proto","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}]
+
+window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当前节点在单位时间内接收到的平均请求数。","name":"节点平均请求数","unit":"个"},{"code":"nodeAvgTrafficOut","description":"当前节点在单位时间内发送的下行流量。","name":"节点平均下行流量","unit":"M"},{"code":"nodeAvgTrafficIn","description":"当前节点在单位时间内接收的上行流量。","name":"节点平均上行流量","unit":"M"},{"code":"nodeHealthCheck","description":"当前节点健康检查结果。","name":"节点健康检查结果","unit":""},{"code":"connectivity","description":"通过区域监控得到的当前IP地址的连通性数值,取值在0和100之间。","name":"IP连通性","unit":"%"},{"code":"groupAvgRequests","description":"当前节点所在分组在单位时间内接收到的平均请求数。","name":"分组平均请求数","unit":"个"},{"code":"groupAvgTrafficOut","description":"当前节点所在分组在单位时间内发送的下行流量。","name":"分组平均下行流量","unit":"M"},{"code":"groupAvgTrafficIn","description":"当前节点所在分组在单位时间内接收的上行流量。","name":"分组平均上行流量","unit":"M"},{"code":"clusterAvgRequests","description":"当前节点所在集群在单位时间内接收到的平均请求数。","name":"集群平均请求数","unit":"个"},{"code":"clusterAvgTrafficOut","description":"当前节点所在集群在单位时间内发送的下行流量。","name":"集群平均下行流量","unit":"M"},{"code":"clusterAvgTrafficIn","description":"当前节点所在集群在单位时间内接收的上行流量。","name":"集群平均上行流量","unit":"M"}]
+
+window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}]
+
diff --git a/web/views/@default/@layout.html b/web/views/@default/@layout.html
index 5543a23a..8a5ec53e 100644
--- a/web/views/@default/@layout.html
+++ b/web/views/@default/@layout.html
@@ -15,7 +15,7 @@
     {$TEA.VUE}
     {$echo "header"}
     
-	
+	
 	
 	
 	
diff --git a/web/views/@default/@layout_popup.html b/web/views/@default/@layout_popup.html
index fea2295b..4aaa4117 100644
--- a/web/views/@default/@layout_popup.html
+++ b/web/views/@default/@layout_popup.html
@@ -12,7 +12,7 @@
 	 
 	{$echo "header"}
 	
-	
+	
 	
 	
 	
diff --git a/web/views/@default/index/index.html b/web/views/@default/index/index.html
index e73d8f66..9b0f97cd 100644
--- a/web/views/@default/index/index.html
+++ b/web/views/@default/index/index.html
@@ -14,7 +14,7 @@
 	
 	
 	
-	
+	
 
 
 
diff --git a/web/views/@default/recover/index.html b/web/views/@default/recover/index.html
index 947f343d..466a5402 100644
--- a/web/views/@default/recover/index.html
+++ b/web/views/@default/recover/index.html
@@ -10,7 +10,7 @@
 	
 	
 	
-	
+	
 	 
 
 
diff --git a/web/views/@default/setup/confirm/index.html b/web/views/@default/setup/confirm/index.html
index 372a380a..71e6584c 100644
--- a/web/views/@default/setup/confirm/index.html
+++ b/web/views/@default/setup/confirm/index.html
@@ -10,7 +10,7 @@
     
     
     
-    
+    
      
 
 
diff --git a/web/views/@default/setup/index.html b/web/views/@default/setup/index.html
index 4d5b8875..8d9a034d 100644
--- a/web/views/@default/setup/index.html
+++ b/web/views/@default/setup/index.html
@@ -10,7 +10,7 @@
 	
 	
 	
-	
+