新企业版整站系统交互场景库
由于涉及组件和场景较多,正在陆陆续续更新中...
本地构建环境: node v0.10.26
| npm 1.4.3
| grunt 0.4.5
构建方法
-
将项目克隆到本地:
git clone git@github.com:ar-insect/ui-library.git
-
切换到项目根目录下面,比如:
cd ~/enterprise
-
执行
npm install
安装项目所需要的插件 -
本地起一个localhost服务,比如访问:
http://localhost/example/singleForm.htm
-
本地开发完成后在
gruntfile.js
里面配置部署(依赖配置在package.json
的alias
) -
在项目的根目录执行
grunt enterprise
完成静态文件的编译和打包 -
最后别忘记在本地测试ok提交代码哦~(提交前先
git fetch origin
git diff master origin/master
确认无误后再执行git merge origin/master
)
Q&A
- 问:这套ui-library主要用来完成什么任务?
答:库里面整合了企业版基础视觉样式和交互组件,不依赖于服务器环境和后端,直接下载到本地开发、部署,在本地完成mockdata调试,最后再将代码提交到svn仓库。
- 问:有没有案例可以参考一下呢?
答:新手请先查看 example
下面到示例了解基本编码规范,然后再阅读gruntfile.js
了解静态文件编译打包相关配置。
- 问:如何在项目中安装arale组件呢?
答:假如你需要在项目中使用arale
的dialog
组件,则可以进入项目根目录中执行spm install arale.dialog
就会自动下载组件以及其依赖到sea-modules
下面,关于组件具体细节可以访问站点arale.org
(注意:以上操作仅限于在内网中使用),然后在项目中这么引用 require('arale/dialog/1.2.6/dialog.js')
也可以将组件配置到package.json
的alias
,比如'dialog':'arale/dialog/1.2.6/dialog.js'这样只需要require('dialog')
就可以了,是不是更加方便了呢^v^
- 问:关于自己开发组件模块的规范是什么呢?
答:现在库里面已经有cellula
fdp
之类的公共模块了,理论上我们在开发环境中会涉及到2大类型的模块,一类是公共的模块,也就是可以供不同系统和业务使用的模块,它们通常是js
底层的类库扩展或者是基于场景模型的构建,比如cellula
fdp
之类,它们存放在lib下面,另一类是纯业务型的模块组件,它们存放在static
下面,而assets
则是存放系统编译打包后的js&css
也就是在线上环境被调用的静态文件就在这里。
本地目录结构
|-- `assets` 静态文件资源库(存放编译打包后的js&css)
|-- `accountswitcher`
|-- `alipay`
|-- `arale`
|-- `bizfundprod`
|-- `cellula`
|-- `enterpriseportal`
|-- `fdp` `Form` `dataView` `paginator`
|-- `gallery`
|- `seajs-style`
|-- `itemList` 一个使用`cellula`做的小玩意儿
|-- `select`
|-- `singleForm` 单表单模型
|-- `tinyscrollbar`
|-- ...
|-- `data`
|-- `example` 示例
|-- `htdocs`
|-- `bizfundprod`
|-- ...
|-- images
|-- `lib` 公共js库
/-- `static` 静态文件
/-- `test` 单元测试
Gruntfile.js 部署脚本
package.js 项目配置
后续优化方案:
- 优化
gruntfile
尽量做到配置最简化
is building...
FDP(1.1.0)是一套非常轻量的富客户查询模型,同时它也支持Single Form
应用场景,内置validate
表单验证模块,支持异步校验,内部依赖cellula v0.4.2
和jQuery 1.7.2
具体源码请参考 lib/fdp/1.1.0
整个模型由FormItem
Form
DataTable
Paginator
等几个模块组成
tips:
-
single Form
只使用form+formItem
组合searchingScene
使用form+formItem+table+paginator
组合 -
1.0.0
由于不支持表单异步验证,故基本废弃掉
FormItem
首先来看下dom结构:
<div class="mi-form-item" id="memo">
<label class="mi-label">备注</label>
<input class="mi-input memo" type="text" data-maxlength="50" name="memo" autocomplete="off" tabindex="7" />
<div class="mi-form-explain">50个汉字,100个字符。</div>
</div>
-
每个
formItem
最外层容器都有唯一的id -
与dom相关的属性:
@hideClass 隐藏的样式,默认为fn-hide
@errorClass 出错的样式,默认为mi-form-item-error
@tipClass 提示的容器样式,默认为mi-form-explain
-
每一个表单控件可以是一个formItem,以下构建一个formItem
/* memo */ var Memo = FormItem.build('Memo', { required : false, type : 'input', label : '备注', defaultTip : '', maxLength : undefined, init : function(conf) { this._super(conf); this.defaultTip = this.tip; this.maxLength = parseInt(this.element.getAttribute('data-maxlength'), 10) * 2; this._bindAll('limit'); $(this.element).on('change keyup', this.limit); this.defaultTip = this.tip; this.addRule('limitWord', function(options, commit) { var msg; var val = options.element.value; var len = val.replace(/[\u4E00-\u9FBF]/g, 'BB').length; if (len > this.maxLength) { msg = this.tip = '输入已超过' + Math.floor((len - this.maxLength) / 2) + '个字。'; this.setMessage('limitWord', msg); return false; } else { if (len == 0) this.tip = this.defaultTip; else this.tip = '还可以输入' + Math.ceil((this.maxLength - len) / 2) + '个字。'; return true; } }); }, limit : function(e) { var _self = this, t = null; if (_self.element.value == '') { this.tip = _self.defaultTip; _self.setDefaultTip(); return; } t = setTimeout(function() { _self.triggerValidate(); clearTimeout(t); }, 50); } });
@type String :目前支持 input
select
radio
checkbox
textarea
// TODO
@label String :表单控件域对应的标签名
@required Boolean :是否校验,如果设置为false
则会跳过校验,但是如果非空同时有验证规则还是会进行校验的
@element Object :控件原生dom节点,如果是一个组合的控件域,则为一个Dom Array,比如,一个日期时间范围的组合控件,this.element[0]
则对应第一个控件的对象,this.element[1]
对应第二个控件的对象,以此类推
@tip 控件域的默认提示,控件初始化时会获取属性tipElement
中的innerHTML
赋给该值
@tipClass String:提示的className
可自定义,默认为mi-form-explain
@tipElement 控件初始化时会根据tipClass
获取到的dom节点赋给该值
@error Function :自定义出错提示
@addRule Function :为表单控件增加校验规则
@setMessage Function :自定义验证规则出错文案
@triggerValidate Function :手动触发校验
- 通过
FormItem.build
方法来构建一个formItem
这个方法接收2个参数:
-
name String :每个
formItem
类的名称,注意不能有重复; -
param Object :类的自定义属性方法和构造函数,其中
init
为类的构造函数;var UserName = FormItem.build('UserName', { type : 'input', label: '用户名', init: function(conf){ this._super(conf); this.addRule('isChinese', function(options) { if (!/[\u4e00-\u9fa5]+/.test(options.element.value)) { return false; } else { return true; } } } });
控件验证
addRule(name, operator, message)
方法可以添加控件自定义校验规则,可一次添加多个规则,支持正则和函数2种模式
Arguments:
-
name String :规则名称
-
operator Function | RegExp :检验执行规则,可以是一个正则表达式或者是一个函数,在函数校验时请返回布尔值作为校验结果,而对于异步校验则需要通过第二个参数
commit
来提交校验结果 -
message String :提示消息
同步验证
this.addRule('isChinese', function(options) {
if (!/[\u4e00-\u9fa5]+/.test(options.element.value)) {
return false; // 校验失败
} else {
return true; // 校验通过
}
}, this.label + '需要包含中文字符');
函数校验:operator
将会接收到一个options对象作为参数
以上也可以使用正则模式:
this.addRule('isChinese', /[\u4e00-\u9fa5]+/, this.label + '需要包含中文字符');
异步验证
this.addRule('availableBatchNo', function(options, commit) {
$.ajax({
url: 'availableBatchNo.json',
type: 'POST',
data : {
userid : 1024
},
success: function(resp) {
if (resp.result) {
commit(true); // 校验通过
} else {
commit(false, '批次号异常错误'); // 校验失败
}
},
error: function() {
commit(false, '系统异常'); // 校验失败
}
});
});
operator 将会接收到两个参数, options 对象 和 commit 函数
- 第一个参数 options 对象中,包含以下字段:
-
options.element
当前在校验的表单项 -
options.display
用户自定义的校验文案
2.第二个参数 commit 函数, 用来提交校验结果. 具有两个参数
-
第一个是 result 对象, 如果校验通过, result 应为一个 truthy 值; 如果不通过 result 应为一个 falsy 值
-
第二个是 msg 字符串, 为提示消息
setMessage(name, message)
设置校验提示信息,若参数为 Object ,则为设置多个。
-
name String :规则名称
-
message String|Object :提示消息
tips:
-
如果需要对控件绑定其它事件则可以在init构造函数初始化中完成
-
formItem对
input
textarea
默认绑定了blur
focus
对radio
checkbox
select
默认绑定了change
Form
以下构建一个Form
var MyForm = Form.build('MyForm', {
type : 'single',
itemList : null,
autoSubmit: false,
init : function(conf) {
this._super(conf);
}
});
@type String :SINGLE
|SEARCH
-
SINGLE 表示单个表单场景
-
SEARCH 表单,数据,分页场景(searchingScene)
@element 表单的原生dom节点
@autoSubmit Boolean :是否自动提交表单,默认为true
@onFormValidated Function :表单验证完成后的自定义事件,如果autoSubmit
设置为false
则可以在这里进入手动提交
var BpToAccountForm = Form.build('BpToAccountForm', {
type : 'single',
itemList : null,
autoSubmit: false, // 手动提交
init : function(conf) {
this._super(conf);
},
onFormValidated: function(options) {
if (options.validate) this.submit(); // 当所有表单项都验证通过进入提交
},
submit: function() {
window.console && console.log('manual submit form.');
if (typeof securityCore !== 'undefined') {
securityCore && securityCore.execute(); // 安全服务化验证
} else {
this.element.submit();
}
}
});
@customSearch Function :自定义查询接口方法,在使用searchingScene
查询场景时使用,例如:
-
这个方法接收一个参数 data:表单所有项的数据集合,这里可以自定义请求方式以及请求前的处理逻辑
/* * Allow the custom query */ customSearch: function(data) { console.log('request:', data); var _self = this; this.ajaxLoadingBox.show(); this.asyn = $.ajax({ url: 'seachingdata.json', type: 'POST', data: data, timeout: 10000, success: function(resp) { console.log('response:', resp); if (resp.status == 'succeed') { _self.ajaxLoadingBox.hide(); _self.dataDispatch(resp); // 收到数据就开始渲染页面了 } else this.emit('FORM:SYSTEMERROR'); }, error: function(xhr, stat, error) { _self.ajaxLoadingBox.hide(); this.emit('FORM:SYSTEMERROR'); window.debug && console.log('error: ', xhr, stat, error); } }); }
@dataDispatch Function :开始准备渲染页面
- 这个方法接收一个参数 data:表示请求由后端返回的json数据
下面通过一个实际的业务场景来说明Single Form
的具体使用:
交互白板
付款账户:是一个控件,如果是主操作员则渲染这个控件,否则直接显示该账户信息,初始化付款账户需要向后端请求该账户余额并作显示
这里让后端给出角色的vo属性,将其放置在dom的data属性上,然后通过js来获取
// 是否为主操作员
var isMainOperator = $('#cardNo').attr('data-ismopt'); // 'true' | 'false'
vm层结构:
<div class="mi-form-item accountCard" id="cardNo" data-ismopt="$!isMainOperator">
<label class="mi-label">付款账户</label>
<input type="hidden" name="cardNo" value="$!alipayCard.cardNo" />
<div class="fn-clear">
#if($!isMainOperator)
<div class="curAccount fn-left">
<span class="ft-bold">#if($!alipayCard.displayAlias)[$!alipayCard.displayAlias] - #end $!alipayCard.logonId</span>
</div>
#else
<div class="fn-left">
<div class="accountSwitch ui-selectmenu fn-hide" role="button" aria-haspopup="true" aria-owns="accountSwitcher" aria-disabled="false">
<span class="fn-left ft-14 ft-bold accountName"></span> <span class="fn-left accountHyphen"> - </span> <span class="fn-left accountEmail"></span>
</div>
<div class="accountSwitcher ui-selectmenu-menu" aria-hidden="false" role="listbox" aria-labelledby="accountSwitch"></div>
</div>
<script type="text/javascript">
// 后端同步输出账户的json数据
window.accountSwitchList = '#SLITERAL($!alipayCardList)';
</script>
#end
<div class="accountAmount fn-left">
<span class="useableBlance fn-hide">(可用余额 <em class="ft-orange"></em> 元)</span>
<span class="usableBalanceAbort fn-hide">(可用余额:系统异常,<em>重新获取信息</em>)</span>
</div>
</div>
<div class="mi-form-explain"></div>
</div>
确定了用户的角色,接下来就是对控件进行初始化了:
var CardNo; // 初始化控件对象
/*
* 付款账户(非主操作员则加载)
*
*/
if (isMainOperator == 'false') {
CardNo = FormItem.build('CardNo', {
type : 'input',
label : '付款账户',
acountList: undefined,
accountSwitcher: null,
getAccountUsablebBalance: getAccountUsablebBalance,
init : function(conf) {
var _self = this, t = null;
this._super(conf);
this._bindAll('trigger', 'triggerValidate');
this.acountList = window.accountSwitchList;
if (this.acountList) this.element.value = this.acountList[0].accountId;
this.accountSwitcher = AccountSwitcher.init({
dataList: this.acountList,
bSwitchDefault: true,
accountSwitchInput: $(this.element)
});
// 因为组件用到`onload`先于`init`执行,所以需要延迟验证。
t = setTimeout(function() {
$(_self.element).trigger('changed');
clearTimeout(t);
}, 50);
$(this.element).on('changed', this.trigger);
this.addRule('usablebBalance', this.getAccountUsablebBalance);
$('.usableBalanceAbort em', this.rootNode).on('click', this.triggerValidate);
},
trigger: function() {
this.triggerValidate();
}
});
}
/*
* 当前账户
*
*/
else if (isMainOperator == 'true') {
CardNo = FormItem.build('CardNo', {
type : 'input',
label : '付款账户',
getAccountUsablebBalance: getAccountUsablebBalance,
init : function(conf) {
this._super(conf);
this._bindAll('triggerValidate');
this.addRule('usablebBalance', this.getAccountUsablebBalance);
this.triggerValidate(); // 初始化获取当前账户可用余额
$('.usableBalanceAbort em', this.rootNode).on('click', this.triggerValidate);
}
});
}
在这里可以看到自定义规则校验usablebBalance
是对当前账户余额的获取并验证,不管是什么角色都需要做这步操作,所以将验证函数提取出来
// 获取账户可用余额并得知余额是否被冻结
var getAccountUsablebBalance = function(options, commit) {
var _self = this;
var cardNo = options.element.value;
$.ajax({
type: 'POST',
url: 'getAccountInfoWithBalance.json',
data: { cardNo: cardNo },
success: function(data) {
if (data.isSuccess) {
$('.useableBlance em', _self.rootNode).text(data.cardInfo && data.cardInfo.availableAmount);
$('.usableBalanceAbort', _self.rootNode).addClass('fn-hide');
$('.useableBlance', _self.rootNode).removeClass('fn-hide');
if (data.cardInfo && data.cardInfo.enablePayment) {
// 账户被冻结,不可转出
commit(false, '该账户余额支付功能关闭,不可将资金转出。');
} else {
commit(true);
}
return;
} else {
$('.useableBlance', _self.rootNode).addClass('fn-hide');
$('.usableBalanceAbort', _self.rootNode).removeClass('fn-hide');
commit(false, '系统异常,请重新登录。');
return;
}
if (data.stat == 'deny') {
// 系统超时
$('.useableBlance', _self.rootNode).addClass('fn-hide');
$('.usableBalanceAbort', _self.rootNode).removeClass('fn-hide');
commit(false, '连接超时,请重新登录。');
}
},
error: function() {
$('.useableBlance', _self.rootNode).addClass('fn-hide');
$('.usableBalanceAbort', _self.rootNode).removeClass('fn-hide');
commit(false, '系统异常,请重新登录。');
}
});
};
这样就可以保证在不同场景下对控件的良好过渡处理,同时也保证了前端对数据验证的可靠性,一旦此验证失败将会出现错误信息,并阻止整个表单的提交
ok,接下面的几个控件就简单多了:
/** 批次号 **/
var BatchNo = FormItem.build('BatchNo', {
type : 'input',
label : '批次号',
init : function(conf) {
this._super(conf);
this.addRule('digits', /^[0-9]+$/, this.label + '应为当天日期开头的11-20位数字,如:2014052021219810')
.addRule('size', /^[0-9]{11,20}$/, this.label + '应为当天日期开头的11-20位数字,如:2014052021219810');
}
});
/** 付款文件 **/
var UploadFile = FormItem.build('UploadFile', {
type : 'input',
label : '付款文件',
init : function(conf) {
this._super(conf);
this._bindAll('trigger', 'repair');
$(this.element).change(this.trigger);
$(this.element).click(this.repair);
this.addRule('availableFileType', function(options) {
var fullpath = options.element.value;
var filename = fullpath.substr(fullpath.lastIndexOf('\\') + 1);
var allow = ['.csv', '.xls'],
extension = /\.[^\.]+$/.exec(fullpath);
if (!extension) {
return false;
}
if ($.inArray(extension[0].toLowerCase(), allow) == -1) {
return false;
}
$('.upload-trigger', this.rootNode).addClass('fn-hide');
$('.rollback-filename', this.rootNode).text(filename).attr('title', filename).removeClass('fn-hide');
$('.reupload-file', this.rootNode).removeClass('fn-hide');
return true;
}, '无效的文件类型,请重新选择' + this.label);
},
trigger: function() {
this.element.blur();
this.triggerValidate();
},
repair: function() {
var _self = this, t = setTimeout(function() {
_self.element.blur();
clearTimeout(t);
}, 500);
}
});
/* 总笔数 */
var TotalCount = FormItem.build('TotalCount', {
type : 'input',
label : '总笔数',
max: undefined,
init : function(conf) {
this._super(conf);
this.max = parseInt(this.element.getAttribute('data-max'), 10);
this.addRule('format', /^[1-9]\d*$/, this.label + '的格式不正确')
.addRule('limit', function(options) {
if (parseInt(this.element.value, 10) <= 0) {
this.setMessage('limit', this.label + '必须大于或者等于1');
return false;
}
if (this.max < parseInt(this.element.value, 10)) {
return false;
}
return true;
}, this.label + '最多' + this.max + '笔');
}
});
/* 总金额 */
var TotalAmount = FormItem.build('TotalAmount', {
type : 'input',
label : '总金额',
reg: new RegExp('^\\d+(\\.\\d{0,2})?$'),
toMoney: undefined,
init : function(conf) {
this._super(conf);
this._bindAll('trigger');
this.toMoney = $('.toMoney', this.rootNode);
$(this.element).on('keyup', this.trigger);
this.addRule('money', this.reg, this.label + '的格式不正确')
.addRule('limit', function() {
if (parseFloat(this.element.value) == 0) {
return false;
}
return true;
}, this.label + '至少为0.01元');
},
trigger: function() {
var val = $.trim(this.element.value);
if (this.reg.test(val)) {
var upperCase = Money.convert(val);
upperCase && this.toMoney.text(upperCase);
this.toMoney.removeClass('fn-hide');
this.setDefaultTip();
} else {
this.error(this.label + '的格式不正确');
this.toMoney.addClass('fn-hide');
}
}
});
/* 备注 */
var Remark = FormItem.build('Remark', {
required : false,
type : 'input',
label : '备注',
defaultTip : '',
maxLength : undefined,
init : function(conf) {
this._super(conf);
this.defaultTip = this.tip;
this.maxLength = parseInt(this.element.getAttribute('data-maxlength'), 10) * 2;
this._bindAll('limit');
$(this.element).on('change keyup', this.limit);
this.defaultTip = this.tip;
this.addRule('limitWord', function(options, commit) {
var msg;
var val = options.element.value;
var len = val.replace(/[\u4E00-\u9FBF]/g, 'BB').length;
if (len > this.maxLength) {
msg = this.tip = '输入已超过' + Math.floor((len - this.maxLength) / 2) + '个字。';
this.setMessage('limitWord', msg);
return false;
} else {
if (len == 0)
this.tip = this.defaultTip;
else
this.tip = '还可以输入' + Math.ceil((this.maxLength - len) / 2) + '个字。';
return true;
}
});
},
limit : function(e) {
var _self = this, t = null;
if (_self.element.value == '') {
this.tip = _self.defaultTip;
_self.setDefaultTip();
return;
}
t = setTimeout(function() {
_self.triggerValidate();
clearTimeout(t);
}, 50);
}
});
这里就是所有formItem的集合,待会将其传给Form对象
/** collection **/
var formElements = {
batchNo : new BatchNo({
key : 'batchNo'
}),
uploadFile : new UploadFile({
key : 'uploadFile'
}),
totalCount : new TotalCount({
key : 'totalCount'
}),
totalAmount : new TotalAmount({
key : 'totalAmount'
}),
remark : new Remark({
key : 'remark'
})
};
if (CardNo) {
formElements.cardNo = new CardNo({
key: 'cardNo'
});
}
定义好formItem
对象后,接下来我们需要定义一个Form对象了
var BpToAccountForm = Form.build('BpToAccountForm', {
type : 'single',
itemList : null,
autoSubmit: false,
init : function(conf) {
this._super(conf);
},
onFormValidated: function(options) {
if (options.validate) this.submit();
},
submit: function() {
window.console && console.log('manual submit form.');
if (typeof securityCore !== 'undefined') {
securityCore && securityCore.execute(); // 安全服务化验证
} else {
this.element.submit();
}
}
});
这里我们还需要对用户安全服务化控件进行验证,所以将Form
的autoSubmit
设置为false
在完成表单项的验证后进入到自定义提交函数submit
中再次提交安全控件的验证
安全服务化控件绑定了form
// 安全服务化
if (window.alipay && alipay.security && alipay.security.core) {
var bpform = $('#bpToAccountForm'), bpSubmit = $('input[type=submit]', bpform);
var disBtn = function() {
bpSubmit.attr('disabled', 'disabled');
bpSubmit.parent().addClass('mi-button-mblue-disabled');
}, enbBtn = function() {
bpSubmit.removeAttr('disabled');
bpSubmit.parent().removeClass('mi-button-mblue-disabled');
};
light.ready(function() {
securityCore = alipay.security.core.init({
form: bpform[0],
stopSubmit: true,
beforeAjaxValidate: function() {
// 按钮设置为不可点击,样式置灰自行完成
disBtn();
},
afterAjaxValidate: function() {
// 按钮设置为可点击,样式自行恢复
enbBtn();
},
block: function() {
// 安全产品校验失败提交区域
disBtn();
},
beforeSubmit: function() {
/* CTU验证 */
sendCTURecheckReq($(bpform).serialize(), checkStep2SecProd);
},
reCheckSuccess: function() {
// 按钮设置为可点击,样式自行恢复
enbBtn();
}
});
});
}
最后实例化Form
var bpToAccountForm = new BpToAccountForm({
key : 'bpToAccountForm',
itemList : formElements // formItems
});
is building...
如有问题或建议请 mail to: ar.insect@gmail.com