记录一次为可能不会算数的运维买单的Web前端开发经历╮(╯_╰)╭。
背景:公司内部开发一个简单的监控平台,可以配置URL访问规则。通过定时任务定时调用自己公司的在线运维平台或第三方平台的访问是否正常,出现问题立即告警通过邮件或短信通知给相关人员。
以上为背景,本人负责开发检查告警的对象及规则配置的后台与前端配置页面的开发。其中有一个告警规则与告警级别的对应是这样的:
- 一个预警规则对应多个预警级别
- 规则和级别的对应中需要设置一个比例范围,作用是根据不同的级别对应范围,触发不同的预警。如一个规则的黄色预警的配置范围是minVal=20%,maxVal=40%。
因此前端配置页面需要勾选不同的级别对应并设置对应项的分配比例。这就扯出了本文的话题,开始时我仅仅处理勾选和设置,但实际测试时测试人员提出个问题,就是他在眼花缭乱中输入此比例时他弄迷糊了,设置的值之间是重复范围的。就是说比如黄色预警的比例是0.1到0.3、红色的比例是0.2到0.4,导致了多次重复报警,然后就要求输入时进行限制(/(ㄒoㄒ)/这……运维时肯定是制定好的值啊……就不能自己算么!)
但是,有强迫症的我感觉既然有这个需求,那就弄呗。(废话一箩筐后,开启本文的正式内容。)
再谈需求
要做限制的话,有哪些限制,也包括了相关的配置需求:
- 配置规则属性时动态载入级别列表(级别存储在数据库中,可手动增删);
- 规则新增或修改时必须选择一个级别对应;
- 可自由选择多个级别,每个级别都能配置当前规则对应级别的一个比例的最大值和最小值;
- 最大值和最小值输入只能为0~1之间的小数(小数点后为两位),包括0和1;
- 同级别设置时,最大值必须大于最小值,最小值必须小于最大值;
- 已经选择的级别输入中,最大值和最小值的范围不能重复冲突。
因设计样式再增加的需求:
- 勾选级别对应后,才能设置对应最大值和最小值;
- 取消勾选后,要清除对应设置的最大值和最小值。
页面设计
因为是内部系统,没有产品原型和UI设计,是沿用的公司的其他平台产品的后台界面,另外这个配置页面则是本人自己设计的。
左侧列表选择要配置的规则,只需注意右下角的预警规则配置。
开发环境
JQuery.js(版本低到我不想说,我的心里话你应该懂得……)、
Google Chrome 63(看这个版本你会更懂我对上面的心情!)、
Bootstrap3、sweetalert。
实现历程之最初
未测试前依据设计,开始实现配置页面,依次实现如下:
1、动态加载并显示级别列表
这里流程很明确,依据我的页面设计,可以在页面加载进来后就通过Ajax加载级别列表,然后显示在固定位置。不过还是有几个要点的。
TL;DR
- 使用了
<template>
做模板进行替换 - 内容生成后再绑定事件
- 增加处理了加载失败的情况,提供链接点击进行重新加载
<template>
:是HTML5标准中新增的标签元素,是一种用于保存客户端内容的机制,该内容在页面加载时不被渲染,但可以在运行时使用JavaScript进行实例化。
在最终写入的位置加入<template>
标签,写入要替换的模板内容:
<div class="form-group col-md-12 no-padding">
<template id="levelRulesTemplate">
<div id="levelRules_{{id}}" data-id="{{id}}" class="input-group input-group-margin">
<span class="input-group-addon">
<input type="checkbox" name="levelRules" value="{{id}}">
</span>
<span class="input-group-addon">{{name}} ⇒</span>
<span class="input-group-addon">最小值:</span>
<input type="text" class="form-control disabled" placeholder="0~1之间,允许2位小数(包含)" value="" disabled />
<span class="input-group-addon">最大值:</span>
<input type="text" class="form-control disabled" placeholder="0~1之间,允许2位小数" value="" disabled />
<span class="input-group-addon">等值:</span>
<input type="text" name="eqVal" class="form-control disabled" placeholder="输入匹配的内容" value="" maxlength="64" disabled />
</div>
</template>
</div>
Ajax加载成功后,获取模板并替换内容后,写入父元素,代码片段如下:
...
load:function (urlKey,dataCache,templateId,help,title,bind) {
...
$.ajax({
type: 'get',
url: url.get(urlKey),
success: function(responseData){
var data=responseData;
//错误时显示重新载入
if(error.has(data)){error.reload(help,title);return;}
//记录数据
dataCache=data.data.list;
//显示level列表
var template=document.getElementById(templateId).innerHTML;
var content="";
for(var i=0;i<dataCache.length;i++){
var temp=template;
for(var key in dataCache[i]){
temp=temp.replace(new RegExp("\{\{"+key+"\}\}","g"),dataCache[i][key]);
}
content+=temp;
}
$("#"+templateId).parent().get(0).innerHTML=content;
//绑定事件的回调
!bind||bind();
},
error:function () {
//错误时显示重新载入
error.reload(help,title);
}
});
}
2、处理勾选和取消勾选
然后处理预警级别勾选和取消勾选的处理。
TL;DR
- 使用数组记录勾选的
checkbox
的值 - 勾选时添加到数组,取消勾选时从数组删除
- 勾选时对应的输入框可输入,取消勾选时清空并禁止输入
checkbox
点击时处理事件:
...
//预警级别选中或取消处理
function levelAction(input) {
//获取父级元素
var parent=$(input).parents("[id^='levelRules_']");
//查找下级中的输入框,进行设置
parent.find("[name$='Val']").each(function () {
$(input).get(0).checked
?($(this).removeAttr("disabled"),$(this).removeClass("disabled"))
:($(this).attr("disabled","disabled"),$(this).addClass("disabled"),$(this).val(""));
});
//记录
$(input).get(0).checked
//添加
?(dataSet.levelSelect.push($(input).val()))
//删除
:(dataSet.levelSelect.splice(dataSet.levelSelect.indexOf($(input).val()),1),rangeSet.remove($(input).val()));
}
3、新建时清空,编辑时设置
在点击顶部的新建按钮进入新建模式,此时需清空级别选择;而点选左侧列表时需根据选择规则中的配置设置级别选择以及设置内容。
...
//通讯事件
var request={
...
//设置规则详情到属性框
set:function (rule) {
...
//对应的预警级别
//清理
$(":checkbox[name='levelRules']").each(function () {
$(this).get(0).checked=false;}
);
$(":text[name$='Val']").each(function () {$(this).val("");});
//设置
if(rule.hasOwnProperty("levelRuleList")){
for(var i=0;i<rule.levelRuleList.length;i++){
var property=rule.levelRuleList[i];
var group=$("#levelRules_"+property.levelId);
var checkbox=group.find(":checkbox[name='levelRules']");
if(!checkbox||checkbox.length==0) continue;
checkbox.get(0).checked=true;
levelAction(checkbox);
group.find("input[name$='Val']").each(function () {
$(this).val(property[$(this).attr("name")]);
});
}
}
},
...
//刷新交互属性区
refresh:function(){
...
//清空全部选择级别
$("#property_panel").find(":checkbox").each(function () {
$(this).get(0).checked=false;
levelAction($(this));
});
}
};
...
//切换成新建模式
function changeCreateAction() {
...
//清空属性框
request.refresh();
}
4、简单限制输入为数字
初次设置时比较简单,只设置了<input>
的类型来限制:
<input type="number" min="0.00" step="0.01" max="1.00" />
实现历程之强化
发现简单设置input的type='number'
其实并无法限制PC上键盘输入字母,而且此时max-length
是无效的。再加上还有上面提到的测试说的限制需求,强化历程如下:
1、限制input的输入字符长度
还是设置<input>
为type='text'
类型,这样可以直接通过max-length
来固定输入长度。
<input type="text" maxlength="4" name="minVal" inputmode="numeric" ... />
限制输入长度为4,因为还有一位小数点。
inputmode
:这里用来定义预期输入的为数字类型,便于理解,同时用于绑定事件。
...
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
...
};
2、限制只能输入数字或小数点
怎么限制输入只能为数字或小数点呢?使用了pattern
属性但是不太管用,而且也不是在输入时限制,只有使用直接“杀掉”的方式了。
...
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
//0.只能输入数字或小数点
this.value=this.value.replace(/[^(\d|\.)]/g,"");
...
};
使用正则表达式匹配非数字和小数点的输入内容,全部替换掉。
3、限制输入数字的范围为0~1的小数
后面就更要复杂点,一位数一位数的判断,也就是一边输入一边判断。
...
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
//0.只能输入数字或小数点
this.value=this.value.replace(/[^(\d|\.)]/g,"");
//1.输入第一位数字只能为0或1
if(this.value.length==1)
this.value=this.value.replace(/[^(0|1)]/,"");
//2.输入第二位数字只能为小数点,并且当第一位为1时无法输入
else if(this.value.length==2){
var first=this.value.substring(0,1);
this.value=(first+this.value.substring(1,2)
.replace(first=="1"?/./:/[^(\.)]/,""));
}
...
};
第一位数字非0或1就替换掉;另外就是当第一位输入1后就不需要输入后面的“.00”的小数位了。
4、检查最大值大于最小值
再比较到小数点后面的位数就涉及到需求中的范围比较了。
TL;DR
- 进入输入框时记录同级别的设置的最小值
- 输入时与当前记录最小值进行对比
- 如果最大值小于最小值提示错误
这里我使用了一个类专门来处理范围比较,便于记录和对比,下面是其中关于最小值的部分:
//rangeSet:对预警级别相关的输入范围进行记录和输入控制判断
var rangeSet={
...
//记录当前的最小值,用于比较最大值的输入
curMin:-1,
//设置当前的最小值
setCurMin:function (min) {
this.curMin=this.subNumber(min);
},
...
//检查输入值是否比当前最小值大
checkMax:function (val) {
return this.subNumber(val)>this.curMin;
},
...
//处理单个数字的截取小数位
subNumber:function (val) {
if(typeof val=="number") return val;
else if(val=="0") val=0;
else if(val=="1") val=100;
else if(val.indexOf(".")==-1) return parseInt(val);
else{
val=val.substring(val.lastIndexOf(".")+1);
val=(val.length==1)?(val+"0"):val;
val=parseInt(val);
}
return val;
}
};
需要注意的是我直接截取了小数点后的两位作为整数进行记录和比较。这样在最大值的input
进入焦点时绑定事件处理:
//最大值输入框进入时,记录对应的最小值
$("input[name='maxVal']").bind("focus",function(){
rangeSet.setCurMin($(this).prevAll("input[name='minVal']").val())
});
输入的时候,进行比较:
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
...
else{
var parent=$(this).parents(".input-group");
var error=false;
//3.输入最大值时检查是否大于最小值
if($(this).attr("name")=="maxVal"){
rangeSet.checkMax(this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的最大值必须大于最小值!"),error=true);
if(error) return;
}
...
}
});
5、检查最小值大于最大值
这步就简单,和上面一样,只不过是反过来:
...
//rangeSet:对预警级别相关的输入范围进行记录和输入控制判断
var rangeSet={
...
//记录当前的最大值,用于比较最小值的输入
curMax:-1,
//设置当前的最大值
setCurMax:function (max) {
this.curMax=this.subNumber(max);
},
...
//检查输入值是否比当前最大值小
checkMin:function (val) {
return this.subNumber(val)<this.curMax;
},
...
};
...
//最小值输入框进入时,记录对应的最大值
$("input[name='minVal']").bind("focus",function(){
rangeSet.setCurMax($(this).nextAll("input[name='maxVal']").val())
});
...
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
...
else{
var parent=$(this).parents(".input-group");
var error=false;
...
//4.输入最小值时检查是否小于最大值
if($(this).attr("name")=="minVal"){
rangeSet.checkMin(this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的最小值必须大于最大值!"),error=true);
if(error) return;
}
...
}
});
6、检查input输入的值是否在已经录入的范围中
同样是记录在上面那个rangeSet
类中,在input
离开焦点时会对重新读取全部设置的级别和范围,用于在再次输入时进行对比。
//rangeSet:对预警级别相关的输入范围进行记录和输入控制判断
var rangeSet={
//记录已用的区间
used:[],
...
//增加一条记录的区间
set:function (id,min,max) {
if(min==""||max=="") return;
this.used.push({id:id,min:this.subNumber(min),max:this.subNumber(max)});
},
//清除一条区间记录
remove:function (id) {
if(!id) return;
var index=-1;
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
if(use.id==id){
index=i;
break;
}
}
index==-1||(this.used.splice(index,1));
},
//记录全部的已用区间
load:function() {
this.used.splice(0,this.used.length);
var _self=this;
//遍历全部选中级别
$(":checked[name='levelRules']").each(function () {
//获取最小值和最大值输入
var parent=$(this).parents(".input-group");
var min=parent.find("[name='minVal']").val();
var max=parent.find("[name='maxVal']").val();
//记录区间
_self.set($(this).val(),min,max);
});
},
...
//验证数字是否可以使用,可以使用返回true
check:function (id,val) {
//区间校验
var result=true,val=this.subNumber(val);
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
if(use.id==id) continue;
if(val>use.min&&val<use.max){
result=false;
break;
}
}
return result;
},
...
};
注意比较时要排除自己这行级别的输入,不然编辑输入修改时也会报错。
在input
离开焦点时,重新记录全部输入:
//范围输入框离开焦点,检查当前的输入
$("input[inputmode='numeric']").bind("blur",function(){
var errorId=rangeSet.checkSubmit();
(errorId!=null)?$("#levelRules_"+errorId).addClass("has-error")
:$(".has-error").removeClass("has-error");
});
输入时进行比较:
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
...
else{
var parent=$(this).parents(".input-group");
var error=false;
...
//5.检查输入的值是否在已经录入的区间
rangeSet.check(parent.attr("data-id"),this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的数值区间已经被设置过了!"));
...
}
});
7、全部输入比对检查是否重复
以上只是解决input
输入时进行比较提醒的情况(其实开始的时候有忽略到这点的,不过已使用很快就会发现),如果原来输入就有问题那么不修改就提交时是不会检查的;另外一个容易忽略的就是两个级别输入的最大值和最小值完全一样的情况。因此在提交保存时需要再对比一次:
...
//rangeSet:对预警级别相关的输入范围进行记录和输入控制判断
var rangeSet={
...
//检查全部输入范围是否正确
checkSubmit:function () {
//先载入全部设置
this.load();
//遍历全部范围值
var result=null;
firstFor:
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
//检查最小值范围
if(!this.check(use.id,use.min)){result=use.id;break;}
//检查最大值与最小值的比较
this.setCurMin(use.min);
if(!this.checkMax(use.max)){result=use.id;break;}
//检查最大值范围
if(!this.check(use.id,use.max)){result=use.id;break;}
//最后一起比较最大值和最小值
for(var j=0;j<length;j++){
var otherUse=this.used[j];
if(otherUse.id==use.id) continue;
if(use.min==otherUse.min
&&use.max==otherUse.max){
result=use.id;
break firstFor;
}
}
}
return result;
},
...
};
这里用了
break tags
跳出多层循环;本文后面会贴出全部这个类的内容。
这样终于满足了全部的限制要求。
附:两块主要部分的全部代码
rangeSet
:用于记录和区间对比的类
//rangeSet:对预警级别相关的输入范围进行记录和输入控制判断
var rangeSet={
//记录已用的区间
used:[],
//记录当前的最小值,用于比较最大值的输入
curMin:-1,
//记录当前的最大值,用于比较最小值的输入
curMax:-1,
//增加一条记录的区间
set:function (id,min,max) {
if(min==""||max=="") return;
this.used.push({id:id,min:this.subNumber(min),max:this.subNumber(max)});
},
//设置当前的最小值
setCurMin:function (min) {
this.curMin=this.subNumber(min);
},
//设置当前的最大值
setCurMax:function (max) {
this.curMax=this.subNumber(max);
},
//清除一条区间记录
remove:function (id) {
if(!id) return;
var index=-1;
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
if(use.id==id){
index=i;
break;
}
}
index==-1||(this.used.splice(index,1));
},
//记录全部的已用区间
load:function() {
this.used.splice(0,this.used.length);
var _self=this;
//遍历全部选中级别
$(":checked[name='levelRules']").each(function () {
//获取最小值和最大值输入
var parent=$(this).parents(".input-group");
var min=parent.find("[name='minVal']").val();
var max=parent.find("[name='maxVal']").val();
//记录区间
_self.set($(this).val(),min,max);
});
},
//检查输入值是否比当前最小值大
checkMax:function (val) {
return this.subNumber(val)>this.curMin;
},
//检查输入值是否比当前最大值小
checkMin:function (val) {
return this.subNumber(val)<this.curMax;
},
//验证数字是否可以使用,可以使用返回true
check:function (id,val) {
//区间校验
var result=true,val=this.subNumber(val);
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
if(use.id==id) continue;
if(val>use.min&&val<use.max){
result=false;
break;
}
}
return result;
},
//检查全部输入范围是否正确
checkSubmit:function () {
//先载入全部设置
this.load();
//遍历全部范围值
var result=null;
firstFor:
for(var i=0,length=this.used.length;i<length;i++){
var use=this.used[i];
//检查最小值范围
if(!this.check(use.id,use.min)){result=use.id;break;}
//检查最大值与最小值的比较
this.setCurMin(use.min);
if(!this.checkMax(use.max)){result=use.id;break;}
//检查最大值范围
if(!this.check(use.id,use.max)){result=use.id;break;}
//最后一起比较最大值和最小值
for(var j=0;j<length;j++){
var otherUse=this.used[j];
if(otherUse.id==use.id) continue;
if(use.min==otherUse.min
&&use.max==otherUse.max){
result=use.id;
break firstFor;
}
}
}
return result;
},
//处理单个数字的截取小数位
subNumber:function (val) {
if(typeof val=="number") return val;
else if(val=="0") val=0;
else if(val=="1") val=100;
else if(val.indexOf(".")==-1) return parseInt(val);
else{
val=val.substring(val.lastIndexOf(".")+1);
val=(val.length==1)?(val+"0"):val;
val=parseInt(val);
}
return val;
}
};
输入检查比较的部分:
//最大值输入框进入时,记录对应的最小值
$("input[name='maxVal']").bind("focus",function(){
rangeSet.setCurMin($(this).prevAll("input[name='minVal']").val())
});
//最小值输入框进入时,记录对应的最大值
$("input[name='minVal']").bind("focus",function(){
rangeSet.setCurMax($(this).nextAll("input[name='maxVal']").val())
});
//范围输入框离开焦点,检查当前的输入
$("input[inputmode='numeric']").bind("blur",function(){
var errorId=rangeSet.checkSubmit();
(errorId!=null)?$("#levelRules_"+errorId).addClass("has-error")
:$(".has-error").removeClass("has-error");
});
//范围输入框数字限制
$("input[inputmode='numeric']").bind("input",function () {
//0.只能输入数字或小数点
this.value=this.value.replace(/[^(\d|\.)]/g,"");
//1.输入第一位数字只能为0或1
if(this.value.length==1)
this.value=this.value.replace(/[^(0|1)]/,"");
//2.输入第二位数字只能为小数点,并且当第一位为1时无法输入
else if(this.value.length==2){
var first=this.value.substring(0,1);
this.value=(first+this.value.substring(1,2)
.replace(first=="1"?/./:/[^(\.)]/,""));
}
else{
var parent=$(this).parents(".input-group");
var error=false;
//3.输入最大值时检查是否大于最小值
if($(this).attr("name")=="maxVal"){
rangeSet.checkMax(this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的最大值必须大于最小值!"),error=true);
if(error) return;
}
//4.输入最小值时检查是否小于最大值
if($(this).attr("name")=="minVal"){
rangeSet.checkMin(this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的最小值必须大于最大值!"),error=true);
if(error) return;
}
//5.检查输入的值是否在已经录入的区间
rangeSet.check(parent.attr("data-id"),this.value)
?(parent.removeClass("has-error"),toastr.clear())
:(parent.addClass("has-error"),toastr.error("输入的数值区间已经被设置过了!"));
}
});