公司内部Ext框架使用注意事项或者叫开发指南

以下内容不全是我一个人理解分析的,感谢前辈们: 陈德林、徐冬冬、大许磊、沈梁以及黄建雄的帮忙!

公司内部Ext框架是老板在Ext基础上封装好的,使用起来极为方便,基本上就是复制粘贴改数据,但前提是你知道在哪里改。

导航

一、喷!!!

why 喷???

喷死你!!!

why ???

喷死你!!!


作为公司主力开发后台管理系统的框架,基本上人人都会写上几次,有的一直写,写的多了习惯了,而有的客串写,就真的想日狗(要日萨摩耶…)。
为什么想日狗???
公司居然没有一份入门指南或者开发注意事项!!!
这简直就是奇葩!!!
市面上任何框架几乎都会写一些使用指南,不管写得好不好。
而公司目前绝大多数项目后台管理系统都是用的该框架,有时候任务急人手不够时需要帮忙写,写过的还好,没写过的就很烦躁了。因为要踩很多坑。
有的坑即使前辈们来帮忙填的时候也很难,因为很有可能不是代码问题,而是缺少配置文件。。。
真的,遇到这种问题心都痛。
想想自己辛辛苦苦找了一两天还是找不到bug,后来发现少两个文件(例如,附件预览需要本地有SWFToolsLibreOffice 5两个文件,不然报错始终看不了。心疼自己,哭哭哭哭哭哭哭哭…)
不要问为什么同事没告诉你,同事不忙吗,偶尔有时间帮你看,他们也很少想到是因为少文件出的错。
balabalabalabala…
算了,不说了,说多了都是泪,我自己写采坑集锦!

二、第一步,看到自己写的页面

由于老板封装好了,所以想看到自己写的页面需要在数据库进行配置。

举例:
1.复制已有的某个页面,其实就是 js 文件。

如上图,Ext.define('gmo.trmprojectDeclare')这里的trmprojectDeclare代表的是你这个js的名字,其中,js文件名应该与它一致。(如果是复制过来的,请改个名字然后去数据库配置。)
editinfo属性一般跟页面标题一样,可以改成自己喜欢的名字。

上面的gmo应该是项目名,暂时我还不知道在哪里配置的。

extend: 'gpersist.base.busform',看到extend顾名思义就知道是继承的意思。其实就是该页面的父类是gpersist.base.busform,所有父类里面的方法他都能直接调用或重写。


从图上可以看到,我的父类都是在 jslib 文件夹下,但是它继承的开头是 gpersist, 我不知道他在哪里配置改名的

OK,接下来需要去数据库配置让我们复制并改名的js在页面上显示

2.数据库配置

首先去T_Sys_Menu_Group这张表里看各个大目录,然后去T_Sys_Menu这张表里根据被我们复制的原始页面标题查询(对应MName字段)。可以得到MID以及MFunction等字段。

      假设查询得到的MID是101,101代表着是T_Sys_Menu_Group这张表里MgID为1的目录下的第一个子菜单
      接下来直接在T_Sys_Menu手动插入数据,假设MID=102没有,那么插入一条数据:
      MID: 102, MPID: 102(一般和MID一样), MgID: 1(父级菜单id), MDisp: 102(一般和MID一样), MCode: 102,
      MFunction(其实就是路径名): 照着101的改。其他字段都差不多,复制粘贴就好。

接下来要给我们新增的页面赋权限。用到T_Sys_Menu_Detail,T_Sys_Role_Detail,T_Sys_Role_Menu这三张表。(正常情况下表名差不多)

      我一直用的最傻的方法,就是手动给三张表加数据,直到开始写这篇文章才知道可以直接在管理系统上赋权限的。。。
第一种方法,表里加数据
      先看T_Sys_Menu_Detail这张表,只有两个字段:AuthIDMIDMID就是我们之前设置好的102,那么AuthID是什么呢?

打开T_Sys_Menu_Auth表,可以看到AuthID对应的名称,其实就是按钮权限。当在T_Sys_Menu_Detail表里设置: MID: 102, AuthID: 1.就代表了该页面能被查看。

      然后是T_Sys_Role_Detail表,这个表里面有个RoleID字段,其实是用来给不同角色分配不同权限的。详情可以查看T_Sys_Role表,这个表里对各个角色进行了分类。

      最后是T_Sys_Role_Menu表,这个表的MAuth字段不大理解什么意思,据组长说貌似是个二进制数据,对应有没有选中。

      好了,数据全部插进去了,这时候页面上还是看不到的,需要去系统里面点击功能操作权限生成,如下图:

      不出意外的话,浏览器上应该能看到刚刚新建的页面了。

第二种方法,直接在系统里点击角色分配

仍然需要先去T_Sys_Menu这张表里插数据。然后不需要手动去另外三张表插数据,直接打开后台管理系统,找到系统管理下的角色管理进行权限设置即可。

三、第二步,修改页面内容并大概理解页面具体生成机制

返回目录

这边打算对每个属性和方法进行分析理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
constructor:function(config){
var me = this; // 保存this,apply方法能够改变this指向
Ext.apply(config,{
editInfo:'项目管理', // 弹出框的标题
winWidth:750, // 这个设置的是弹出窗的宽度
winHeight:450, // 这个设置的是弹出窗的高度
hasColumnUrl:true, // 说实话,我改为false页面依然能用没报错,不知道有什么用。busform.js和baseform.js都没有这个属性
columnUrl:'SysSqlColumn.do?sqlid=p_get_column', // 一般页面都是列表形式展示,这里接口获得列表页的列名,
// 在T_Set_Sql_Column表里面配置,其中p_get_column是存储过程
storeUrl:'Search', // Search接口,用来获取数据放到页面上的
saveUrl:'', // 顾名思义,这里是保存接口
expUrl:'', // 导出接口,看各个页面上几乎都是Search接口
deleteUrl:'', // 删除接口
checkUrl: '', // 审核接口
<!-- 分割,美观 -->
idPrevNext:'', // 暂时不清楚。父类中在 OnLoadData 方法里判断 record.idPrevNext 是否有值,有的话通过 OnSetData
// 返回 true; 也在 OnFormEdit 方法中判断 idPrevNext 是否为空,不为空设置
// me.dataDeal = gpersist.DATA_DEAL_EDIT 以及判断
// me.OnSetData(tools.GetValue(mep + me.idPrevNext)) 是否为true,
// (这里跟踪到 tools.GetValue() 发现用了 Ext.getCmp(id).getValue(), 这是 Ext 自己的方法,
// 可以得到一个 String 值。)
// 为true的话调用 OnAuthEditForm 方法, me.OnAuthEditForm(2, true);
// OnAuthEditForm方法管理 页面空间授权处理,由于传入的是 2 即 DATA_DEAL_EDIT ,所以才去授予的权限
<!-- 分割,美观 -->
hasDetail: true, // 父类默认是 false ;然后在 OnFormLoad() 加载主页面方法里判断是否为 true,
// true 的话调用 pleditview.add(me.plDetail) 同时继续判断 me.plDetail && !me.hasEdit 是否为
// true, true的话应该是让 me.plDetail 面板居中的意思
// pleditview.add(me.plDetail) 方法其实是在 pleditview 面板里加一个 plDetail 面板;
// 然后在 OnCreateEditWin() 方法里再次判断 hasDetail 是否为 true, true 的话创建了几个面板,
// 调用了 OnGetDetailFunction(), OnBeforeCreateDetail(), OnAfterCreateDetailToolBar(),
// OnAfterCreateDetail() 四个方法,应该是进行面板初始化创建的。
// 之后在 OnGetSaveParams() 方法里也判断了 hasDetail 是否为 true, true的话把
// me.plDetailGrid.store.getAt(i).data的数据循环放进新数组 details 里面,
// 把 { details: Ext.encode(details) } 赋值给 me.saveParams。
// 其中 Ext.encode()函数将Json对象转换成Json格式字符串。([1,2] ==> "[1,2]")
// { details: Ext.encode(details) } 是一个对象, 第一个 details 是该对象的属性名,
// 不是之前的那个数组,注意下!
<!-- 分割,美观 -->
hasDetailEdit: true, // 默认 false ;先在 OnCreateEditWin() 方法里判断是否为 true, true的话调用
// me.plDetailGrid.insertDocked(0, tbdetailedit)
// tbdetailedit 是一个头部工具栏,insertDocked()方法没找到,可能是Ext自带的,这个方法的意思应该
// 是在 plDetailGrid 面板中插入一个头部工具栏。
<!-- 分割,美观 -->
hasPage: true, // 默认是true,先在 OnBeforeFormLoad() 方法中判断(该方法用于在加载页面前初始化工具栏的)。
// 如果判断为true,使用Ext.Array.insert()方法,这是Ext的扩展工具方法,用于在数组中插入
// 一个新数组。
// 经过试验,发现把 hasPage 设为 false,页面底部的分页没有了。所以这个属性可以控制分页。
// 研究父类发现是在 OnFormLoad() 方法里控制的。
// 在 OnSearch() 方法里也有用到。
<!-- 分割,美观 -->
hasExit: true, // 父类没找到该方法。改为false页面无报错能正常展示。
height:320, // 父类没有,不知道哪来的。。。估计是自己定义的吧
hasDetailGrid:true, // 父类没有
hasDetailEditTool: true, // 父类没有
hasPrevNext: false, // 默认是 true, 先在 OnCreateEditWin() 方法中判断,true的话加 上一条和下一条 共两个按钮
hasGridSelect: true, // 默认是 false, 主要在 OnFormLoad() 方法中有这么一个判断:
// selModel: me.hasGridSelect ? new Ext.selection.CheckboxModel({ injectCheckbox: 1 })
// : {}, 查了下,这是控制页面列表默认选中第几行的。true默认显示第一行,false默认不选中。
<!-- 分割,美观 -->
hasDetailCheck: false, // 默认是 true, 在 OnCreateEditWin() 里作为参数传入 tools.GetGridColumnsByUrl() 方法,
// 看了下 tools.GetGridColumnsByUrl() 方法,发现如果 hasDetailCheck 为 true 的话,
// 会创建 checkbox 列。
<!-- 分割,美观 -->
hasDetailGridSelect:true, // 默认是 false, 在父类上看到,该属性与 hasDetailCheck 一起使用的。
});
me.callParent(arguments); // 这一步调用父类的方法,同时覆盖同名的方法。
},

接下来开始构建页面,一般紧接着 constructor() 的是 OnBeforeFormLoad()

返回目录

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
OnBeforeFormLoad:function(){
var me = this;
var mep = me.tranPrefix;
me.plLeft= null,
me.plRight= null,
me.attach4 = [],
me.OnInitGridToolBar();
var item1 = [
' ',{xtype:'textfield',fieldLabel:'项目名称',labelWidth:70,width:220,maxLength:20,
name:'searchprojectname',id:mep+'searchprojectname',allowBlank:true},
' ', tools.GetToolBarCombo('searchbasetype', mep + 'searchprojecttype', 180, '项目类型',
60, tools.ComboStore('ProjectType',gpersist.SELECT_ALL_VALUE)),
];
var item2 = [
// 搜索按钮
' ', { id: mep + 'btnSearch', text: gpersist.STR_BTN_SEARCH, iconCls: 'icon-find', handler: me.OnSearch, scope: me },
// 新增按钮
'-', ' ', { id: mep + 'btnAdd', text: gpersist.STR_BTN_NEW, iconCls: 'icon-add', handler: me.OnNew,scope: me },
// 复制按钮
' ', { id: mep + 'btnCopy', text: gpersist.STR_BTN_COPY, iconCls: 'icon-add', handler: me.OnCopy,scope: me},
// 编辑按钮
' ', { id: mep + 'btnEdit', text: gpersist.STR_BTN_EDIT, iconCls: 'icon-edit', handler: me.OnEdit,scope: me},
// 删除按钮
' ', { id: mep + 'btnDelete', text: gpersist.STR_BTN_DELETE, iconCls: 'icon-delete', handler: me.OnDelete,scope: me},
// 处理按钮(这里发现页面上处理按钮没有展示出来,很有可能是 id 冲突了,试试把 id 改了)
'-', ' ', { id: mep + 'btnDeal2', text: gpersist.STR_BTN_DEAL, iconCls: 'icon-deal', handler: me.OnDeal, scope: me },
// 审核按钮(这里发现页面上处理按钮没有展示出来,很有可能是 id 冲突了,试试把 id 改了)
'-', ' ', { id: mep + 'btnCheck1', text: '审核', iconCls: 'icon-audit', handler: me.OnCheck,scope: me},
// 取消审核按钮(不出现的话可能与审核按钮那边一样)
' ', { id: mep + 'btnUnCheck1', text: '取消审核', iconCls: 'icon-unaudit', handler: me.OnUnCheck,scope: me},
// 导出按钮
'-', ' ', { id: mep + 'btnExport', text: gpersist.STR_BTN_EXPORT, iconCls: 'icon-export', handler: me.OnExport, scope: me },
// 打印按钮
' ', { id: mep + 'btnPrint', text: gpersist.STR_BTN_PRINT, iconCls: 'icon-print', handler: me.OnPrint, scope: me },
// 刷新按钮,页面上是个小球的图标
'-', ' ', { tooltip: gpersist.STR_BTN_REFRESH_NOW, iconCls: 'icon-pagerefresh', handler: me.OnResetForm, scope: me }
];
tools.SetToolbar(item1,mep);
tools.SetToolbar(item2,mep);
var toolbar1 = Ext.create('Ext.toolbar.Toolbar',{items:item1,border:false});
var toolbar2 = Ext.create('Ext.toolbar.Toolbar',{items:item2,border:false});
me.tbGrid.add(toolbar1);
me.tbGrid.add(toolbar2);
},

如上图所示(上图并不对应我们代码里的内容,只是举个列子),项目编号和项目名称这一行对应item1,第二行的查询等按钮对应item2

返回目录

接下来分析一波tools.SetToolbar(item1,mep)以及me.tbGrid.add(toolbar1)
tools.SetToolbar()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tools.SetToolbar = function (items, mcode) {
//var sauth = tools.GetMenuAuth(mcode);
var sauth = tools.GetAuth(mcode);
// 增加
tools.DeleteToolbarItemByAuth(sauth, items, mcode + 'btnAdd', 2, tools.ValidSeparator(sauth, 14));
tools.DeleteToolbarItemByAuth(sauth, items, mcode + 'btnCopy', 2);
...
}
tools.GetAuth = function(mcode) {
var rtn = 0;
if (gpersist && gpersist.UserInfo && gpersist.UserInfo.auths) {
Ext.each(gpersist.UserInfo.auths, function (auth, index) {
if (auth.mcode == mcode) {
rtn = auth.mauth;
return;
}
});
}
return rtn;
};

如上所示,tools.SetToolbar(item1,mep)传入了item1mep,item1我们知道了,但是mep呢?
var mep = me.tranPrefix即把me.tranPrefix赋值给mep,那么tranPrefix是什么?

      继续看父类,不是在 busform.js 里定义的,那就去 baseform.js 里找。果然,抬头就看见tranPrefix: '0000',也就是说这个属性初始值是个String,但是有什么用呢?不急,继续往下看呗。
      在 constructor() 就开始用了 me.OnInitConfig(); me.sauth = tools.GetMenuAuth(me.tranPrefix),可是初始化的值不是’0000’吗,传过去干嘛的呢?

返回目录

看看tools.GetMenuAuth()方法呗

1
2
3
4
5
6
7
8
9
10
11
12
tools.GetMenuAuth = function(mcode) {
var sauth = '';
if (gpersist && gpersist.UserInfo && gpersist.UserInfo.auths) {
Ext.each(gpersist.UserInfo.auths, function (auth, index) {
if (auth.mcode == mcode) {
sauth = auth.mauth.toString(2);
return false;
}
});
}
return sauth;
};

      假设把’0000’传了进去,接下去走,需要判断gpersist && gpersist.UserInfo && gpersist.UserInfo.auths为true,这时候又需要去看看gpersist.UserInfogpersist.UserInfo.auths 是什么了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
gpersist.OnInit = function() {
//if (!eval(gpersist.CLASS_BASE))
// gpersist.LoadJs([gpersist.URL_BASE_CLASS], gpersist.NullCallBack, '');
if (!Ext.isDefined(gpersist.AllParams))
gpersist.AllParams = Ext.create('Ext.util.MixedCollection');
gpersist.GetSessionUser();
};
gpersist.InitUserInfo = function() {
gpersist.UserInfo = null;
};
gpersist.GetUserInfo = function() {
if (gpersist.UserInfo)
return gpersist.UserInfo;
else
return null;
};
gpersist.GetSessionUser = function () {
if (!Ext.isDefined(gpersist.UserInfo)) {
var record = tools.JsonGet(gpersist.ACTION_GET_ONLINEINFO);
if (record && record.data)
gpersist.UserInfo = record.data;
}
};
tools.JsonGet = function (url, params) {
var data = [];
params = params || {};
Ext.Ajax.request({
url: url,
params: params,
async: false,
success: function (response, opts) {
//alert(response.responseText);
try {
data = Ext.decode(response.responseText.replace(/\r\n/g,'<br/>').replace(/\"(new Date\((-*)\d+\))\"/g,'$1'));
}
catch(ex) {
alert("JsonGet:" + ex.toString());
}
},
failure: function (response) { }
});
return data;
};

      从上面代码分析可以得知,在OnInit()方法中调用了GetSessionUser()方法,在GetSessionUser()里判断UserInfo属性有没有定义,如果没有定义,通过tools.JsonGet()方法调后台接口(gpersist.ACTION_GET_ONLINEINFO就是我们定义的后台接口地址),得到返回数据。然后对UserInfo属性进行赋值。
      接下来继续回过去看GetMenuAuth()方法就很好理解了,它遍历了gpersist.UserInfo.auths,通过满足auth.mcode == mcode条件来对sauth赋值并返回。

     但是,经过我在页面上打印输出发现tranPrefix并不是’0000’,这就说明在GetMenuAuth()之前已经通过掉后台接口改变了tranPrefix值。可是,无论我怎么翻看baseform.js和busform.js都找不到任何一个在这之前调后台接口并给tranPrefix赋值的方法,百思不得其解,很郁闷。
    最后,去请教了陈德林老师,经过陈老师一讲,瞬间明白了。我只盯着一个页面,却忘记了项目是从登录开始的,项目真正的基础是basemain.js!!!在basemain.js里面调方法接收后台数据,所以像tranPrefix这样的属性已经通过basemain.js里面的方法进行了赋值。
为了接下来能跑的通,理解的通,我们需要去查看basemain.js了。

      代码太多,就不怎么贴出来了。basemain.js构造函数里面一上来就通过gpersist.GetSessionUser()进行赋值;然后又调用了OnLogin()方法,进而调用了gpersist.LoadJs()方法登录。
      然后通过topToolbar.add(me.OnCretaeMenu())增加了OnCretaeMenu()方法。

那么,什么时候对tranPrefix进行赋值的呢?basemain.js里并没有可用的tranPrefix,唯一的还被注释掉了。这里我们好像又断了头绪,怎么办?

这里貌似并没有什么好的查询方法,要么全局搜,要么一个方法一个方法的看。好在运气不错,在gpersist.js里面找到了对tranPrefix赋值的操作。

OnMenuClick()
返回目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// basemain.js
OnMenuClick: function(data) {
if (data.leaf) {
if (!data.istab) {
gpersist.OnMenuClick(data);
return;
}
...
listeners: {
'close': function() {
if(Ext.isIE) {
CollectGarbage();
}
},
'afterrender': function() {
gpersist.OnMenuClick(data);
}
}
// gpersist.js
gpersist.OnMenuClick = function(menu) {
if (menu && menu.url) {
var menubean = menu.url.lastIndexOf("/") >= 0 ? menu.url.substring(menu.url.lastIndexOf("/") + 1) : menu.url;
var prefix = gpersist.STR_SYS_NAME + '.';
if (menu.mid >= 8000)
prefix = 'gpersist.';
if (eval(prefix + menubean)) {
Ext.create(prefix + menubean, {mid:menu.mid,tranPrefix:menu.mcode});
}
else {
gpersist.LoadJs([menu.url + '.js'], gpersist.DefaultTranCallBack, menu);
}
}
}
gpersist.DefaultTranCallBack = function() {
var menu = gpersist.JsLoadCallBackParam;
if (menu && menu.url) {
var menubean = menu.url.lastIndexOf("/") >= 0 ? menu.url.substring(menu.url.lastIndexOf("/") + 1) : menu.url;
var prefix = gpersist.STR_SYS_NAME + '.';
if (menu.mid >= 8000)
prefix = 'gpersist.';
if (eval(prefix + menubean)) {
Ext.create(prefix + menubean, {mid:menu.mid,tranPrefix:menu.mcode});
}
}
};

      从上可以看出,因为OnMenuClick()是定义在basemain.js里面的方法,而在basemain.js里面的OnMenuClick()里又调用了gpersist.OnMenuClick(data),观察gpersist.OnMenuClick()方法:
      如果eval(prefix + menubean)存在的话对tranPrefix赋值为menu.mcode,
如果不存在调用gpersist.LoadJs([menu.url + '.js'], gpersist.DefaultTranCallBack, menu)方法。依然会对tranPrefix赋值为menu.mcode

结合名称可以知道是在点击目录的时候调用该方法并进行了赋值。那怎么给点击事件绑定OnMenuClick()方法呢?

结合页面点击分析,我们在点击页面左侧目录栏的时候会触发该事件,那么找到左侧目录栏应该就能找到绑定的点击事件了。

      这时候又需要我们去找了,确实蛮难找的。好在有全局搜索,我们发现除了basemain.js和gpersist.js里面有该方法,还在main.jshmain.js里面调用了该方法。
      先打开main.js,继承自basemain.js,在GetSubMenus()里面通过handler: function () { me.OnMenuClick(menu); }来调用的,这个看方法名就跟点击没啥大关系;
      那在打开hmain.js吧,果然,它重写覆盖了OnCretaeMenu()方法,在该方法里通过:

1
2
3
4
5
listeners: {
'itemclick': function (view, record) {
me.OnMenuClick(record.data);
}
}

调用OnMenuClick()方法来实现对tranPrefix赋值。(一个方法套着一个方法,找起来确实麻烦)
还记得OnCretaeMenu()方法在哪里调用的吗?重新看basemain.js,在构造函数里就调用了哦。

tranPrefix赋值我们知道了,可是才打开冰山一角,继续分析tools.SetToolbar(),通过var sauth = tools.GetAuth(mcode)得到sauth,然后调用tools.DeleteToolbarItemByAuth(sauth, items, mcode + 'btnAdd', 2, tools.ValidSeparator(sauth, 14));

tools.DeleteToolbarItemByAuth()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tools.DeleteToolbarItemByAuth = function (auth, items, id, idx, separator) {
if (!tools.ValidMenuAuth(auth, idx)) {
for (var i = 0; i < items.length; i++) {
if (items[i].id && (items[i].id == id)) {
if (i < items.length) {
items.remove(i);
if (i > 0)
items.remove(i - 1);
if (separator && (i > 1))
items.remove(i - 2);
}
break;
}
}
}
};

      这个方法开头调用tools.ValidMenuAuth(auth, idx),该方法默认返回false。这一步应该是进行删除按钮。通过传入的tranPrefix来进行删除按钮(其实就是通过权限值来控制按钮的删除)
      me.tbGrid.add(toolbar1)这一步就是把需要的按钮加入页面展示。

OnBeforeFormLoad()先到此为止,接下来就是OnBeforeCreateEdit()
返回目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
OnBeforeCreateEdit:function(){
var me = this;
var mep = this.tranPrefix;
var nowdate=new Date();
var attritems = [
' ',
{
xtype: 'textfield',
fieldLabel: '项目编号检索', // 对应下图2 试验基地名称 字段位置(图与代码并不一致请注意)
labelWidth: 100, // 对应下图2 试验基地名称 字段长度(图与代码并不一致请注意)
width: 220, // 对应下图2 试验基地名称 字段后面的 输入框 长度(图与代码并不一致请注意)
maxLength: 12, // 对应下图2 试验基地名称 字段后面的 输入框 允许输入的最大长度(图与代码并不一致请注意)
name: 'searchprojectid',
id: mep + 'searchprojectid',
allowBlank: true // 允许有空值
},
' ',
{
text: gpersist.STR_BTN_SEARCH, // 查询按钮文字
iconCls: 'icon-find', // 查询按钮图标
handler: me.OnPrdIdSearch, // 触发查询方法
scope: me // 作用域
}
];
// 类似下图
var attr = Ext.create('Ext.ux.GridPicker', {
fieldLabel: tools.MustTitle('项目编号'), // 搜索框对应的名称(类似下图1的'试验基地名称')
name: 'project.projectid',
id: mep + 'projectid',
winTitle: '项目编号', // 搜索弹出框的标题(类似下图2的'试验基地')
maxLength: 20, // 输入框允许输入最大长度
maxLengthText: '项目编号长度不能超过20个字符!', // 输入框提示
selectOnFocus: false, // 这个属性应该是选择框聚焦
labelWidth: 120, // 类似下图1'试验基地名称'这段字的长度(出现折行说明长度不够)
blankText: '项目编号不能为空!', // 提示不为空
allowBlank: false, // 不允许有空值
anchor: '100%',
tabIndex: 3,
editable: false,
columnUrl: 'SysSqlColumn.do?sqlid=p_get_projectid', // 搜索弹出框列表列名
storeUrl: 'SearchProjectid', // 搜索弹出框数据获取接口
hasPage: true,
pickerWidth: 800,
pickerHeight: 450,
searchTools: attritems // 对应下图2 试验基地名称以及查询按钮一行(图与代码并不一致请注意)
});
me.editControls=[
{
xtype:'container',anchor:'100%',layout:'column', items:[
{ xtype:'container',columnWidth:.5,layout:'anchor', items:[
tools.FormText('项目名称', 'project.projectname', mep+'projectname', 20, '96%', false, 1, null, 100)
]},
{xtype:'container',columnWidth:.5,layout:'anchor', items:[
attr
]},
]
},
tools.FormTextArea('备注','project.remark',mep+'remark',400,'100%',false,20,5,100),
{xtype:'hiddenfield',name:'project.deal.action',id: mep + 'datadeal' },
];
me.disNews = ['projectname']; // 不能新增
me.disEdits = ['projectname']; // 不能编辑
me.enNews = ['projectid']; // 能新增
me.enEdits = ['projectid']; // 能编辑
attr.on('griditemclick', me.OnPrdIdSelect, me); // 给搜索框绑定选择方法
attr.on('gridbeforeload', me.OnPrdIdBeforeLoad, me); // 给搜索框绑定加载方法
},

attr类似下图



具体的tools.FormText()等方法可自行查看,一般最后一个参数代表字段长度,不是输入框哦

      {xtype:'hiddenfield',name:'project.deal.action',id: mep + 'datadeal' },这一段代码代表隐藏,点击保存按钮时依然会向后台传参数。如果以后有后台需要但是前台并不需要展示的字段可以使用这种写法,如果采用该写法就不需要把相同的字段写在在items:[]里面了。

主要看OnPrdIdSelect(),OnPrdIdBeforeLoad()以及OnPrdIdSearch()方法
返回目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 选择框并且设置值
OnPrdIdBeforeLoad: function () {
var me = this;
var mep = this.tranPrefix;
var prdid = Ext.getCmp(mep + 'projectid');
if (prdid.store) {
prdid.store.on('beforeload', function (store, options) {
Ext.apply(store.proxy.extraParams, {
'project.projectid': tools.GetValueEncode(mep + 'searchprojectid')
});
});
}
},
OnPrdIdSelect: function (render, item) {
var me = this;
var mep = this.tranPrefix;
// console.log("item3",item);
if (item && !Ext.isEmpty(item.projectid)) {
tools.SetValue(mep + 'projectid', item.projectid);
tools.SetValue(mep + 'projectname', item.projectname);
}
},
OnPrdIdSearch: function () {
var me = this;
var mep = this.tranPrefix;
me.OnPrdIdBeforeLoad();
var prdid = Ext.getCmp(mep + 'projectid');
prdid.store.loadPage(1);
}

      经过测试发现,先调用的是OnPrdIdBeforeLoad(),与我先前所预料的OnPrdIdSearch()不一致。先调用了OnPrdIdBeforeLoad()方法把search接口查到的数据展示在列表上,然后调用了OnPrdIdSelect()方法进行赋值,可以发现页面上的确有值保存成功。我将OnPrdIdSearch()注释掉也丝毫没问题。

1
2
3
Ext.apply(store.proxy.extraParams, {
'project.projectid': tools.GetValueEncode(mep + 'searchprojectid')
});

      prdid.store.on('beforeload', function (store, options) {...},这段代码其实给store加了一个beforeload监听事件。
      OnPrdIdBeforeLoad()中的上段代码其实是将'project.projectid'属性及属性值拷贝给store.proxy.extraParams对象。
      但是,经过我测试发现,打印prdid.store居然是undefined!!!(我测试时搜索弹出框里面的数据少且没有分页)我试着将OnPrdIdBeforeLoad()里面从var prdid以及if判断全部隐藏,发现没有报错且能正常赋值显示,所以暂时并不清楚它到底干嘛的。
      而OnPrdIdSelect则是在双击选中时调用,我打印了item,发现该对象里面的属性以及属性值与search接口返回的对应的该条数据一样。但是我们并不清楚它是如何做到的。
      这时候我们需要回过去看attr.on('griditemclick', me.OnPrdIdSelect, me);这段代码,经过百度发现这段代码其实是给attr框里面的单行添加了双击绑定事件,即双击某一行调用OnPrdIdSelect()方法;但是我们仍然没有看到它是如何传参的,那就继续呗。
      紧接着我打印了render发现是一个constructor(),其实就是attr


接下来看看OnInitData()方法
返回目录

1
2
3
4
5
6
7
8
OnInitData : function() {
var me = this;
var mep = me.tranPrefix;
console.log('OnInitData')
me.callParent(arguments);
tools.SetValue(mep + 'projectid', '1');
tools.SetValue(mep + 'projectname', '1');
},

该方法其实是用来初始化赋值的即刚进入页面某些字段就要有初始值。但是该方法应该在接下来的OnLoadData()调用,不然无法赋值。

看看父类busform.js

1
2
3
4
5
6
7
8
9
10
OnInitData : function() {
var me = this;
me.plEdit.getForm().reset();
me.OnDetailRefresh();
},
OnDetailRefresh : function() {
var me = this;
if (me.plDetailGrid && me.plDetailGrid.store)
me.plDetailGrid.store.load();
},

      父类调用了两个方法,一个重置,另一个刷新。me.plDetailGrid.store.load()经过搜索应该是Ext自带的方法,用来与后台交互获取数据加载页面。单独来看的话不容易观察,接下来先看OnFormLoad()方法。

OnFormLoad()方法

返回目录

该方法用来加载主页面,算是页面主体方法了。

busform.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 加载主页面
OnFormLoad : function() {
var me = this;
var mep = me.tranPrefix;
var date = new Date();
var mainpanel = Ext.getCmp('tpanel' + me.mid);
if (!Ext.isDefined(mainpanel))
return;
mainpanel.removeAll();
tools.log('OnFormLoad1', date);
// 生成列表的字段属性
me.columns = [];
me.fields = [];
tools.GetGridColumnsByUrl(this.columnUrl, me.columns, me.fields, mep + '_l_');
me.gridStore = tools.CreateGridStore(tools.GetUrl(this.storeUrl), me.fields);
// 加载主页面前
me.OnBeforeFormLoad();
tools.log('OnFormLoad2', date);
// 生成列表
me.plGrid = Ext.create('Ext.grid.Panel', {
id : mep + 'grid',
region : 'center',
frame : false,
border : false,
margins : '0 0 0 0',
padding : '0 0 0 0',suspendLayout: false,
loadMask : true,
columnLines : true,
viewConfig : {
autoFill : true,
stripeRows : true
},
columns : me.columns,
store : me.gridStore,
tbar : me.tbGrid,
enableColumnMove: false,// suspendLayout:true,//延时操作,不去掉的话column不能拖动
selModel: me.hasGridSelect ? new Ext.selection.CheckboxModel({ injectCheckbox: 1 }) : {},
listeners : {
'itemdblclick' : { fn : me.OnShow, scope : me },
'itemclick' : { fn : me.OnItemClick, scope : me }
}
});
// 分页处理
if (me.hasPage) {
me.plGrid.insertDocked(0, Ext.create('Ext.PagingToolbar', {
store : me.gridStore,
displayInfo : true,
displayMsg : gpersist.STR_PAGE_FMT,
emptyMsg : gpersist.STR_NO_DATA, suspendLayout: true,
dock : 'bottom'
}));
}
// 主界面生成后处理
me.OnAfterFormLoad();
tools.log('OnFormLoad2', date);
// 建立编辑界面
me.OnCreateEditWin();
tools.log('OnFormLoad3', date);
var pleditview = Ext.create('Ext.Panel', {
frame : false,
autoScroll : false,
region : 'center',
border : false,
layout : 'border',
margins : '0 0 0 0',
padding : '0 0 0 0'
});
if (me.hasEdit)
pleditview.add(me.plEdit);
if (me.hasDetail) {
if (me.plDetail && !me.hasEdit)
me.plDetail.region = 'center';
pleditview.add(me.plDetail);
}
tools.log('OnFormLoad4', date);
me.tabMain = Ext.create('Ext.tab.Panel', {
border : false,
activeTab : 0,
bodyBorder : false,
defaults : {
bodyStyle : 'border:0px;padding:0px;'
},
margins : '0 0 0 0',
region : 'center',
deferredRender : me.deferredRender,
items : [me.plGrid, pleditview]
});
tools.log('OnFormLoad6', date);
me.tabMain.getTabBar().setVisible(false);
tools.log('OnFormLoad5', date);
mainpanel.add(me.tabMain);
tools.log('OnFormLoad7', date);
if (me.hasAutoLoad)
me.OnSearch();
tools.log('OnFormLoad8', date);
},

baseform.js

1
2
3
4
5
6
7
8
9
10
11
constructor : function(config) {
var me = this;
me.callParent(arguments);
me.OnInitConfig();
Ext.apply(me, config);
me.sauth = tools.GetMenuAuth(me.tranPrefix);
tools.GetParams(me.selectParams);
me.OnFormLoad();
},
OnFormLoad : function() {
},

      首先,busform.js继承自baseform.js,所以先去baseform.js看有没有OnFormLoad()。观察上面的代码发现baseform.js里的确定义了OnFormLoad()方法但是里面什么都没有,专门用来给子类重写覆盖的。而且是在constructor()里面直接调用的,即页面一加载就调用该方法了。(或者说实例化一个对象就调用了该方法)
      现在再看看busform.js以及它的继承者,发现几乎就是copy。

  • var mainpanel这一步就是在创建主面板(类似画画一样,先弄个面板出来)
  • mainpanel.removeAll()这一步是为了以防万一,先清空面板为接下来画页面做准备。
  • tools.GetGridColumnsByUrl(this.columnUrl, me.columns, me.fields, mep + '_l_')这一步调用tools工具里面的方法,代码较长不列出来,其实主要是调接口接收数据,然后给me.columnsme.fields赋值,用于接下来创建列表列名。
  • 然后是tools.CreateGridStore(tools.GetUrl(this.storeUrl), me.fields),我把CreateGridStore()方法的代码列在下面。其中Ext.data.JsonStore是自带的方法,继承自Ext.data.Store。我打印了me.gridStore发现是一个constructor()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tools.CreateGridStore = function (url, fields, pagesize) {
var size = pagesize || 20;
return new Ext.data.JsonStore({
proxy: {
type: 'ajax',
url: url,
timeout: 180000,
reader: {
type: 'json',
root: 'data', // 表示从后台传过来的json数据
totalProperty: 'total' // 表示结果数
}
},
autoLoad: false,
sortOnLoad: true,
fields: fields, // 对象数组集合
pageSize: size
});
};
  • 接下来调用OnBeforeFormLoad()方法,前面有讲过。
  • me.plGrid用于生成列表,之前有碰到过需求页面上不要列表,直接可编辑。其实就在这里给它注释掉,当然,下面的me.tabMain里面的items也要把plGrid删掉。
    需要注意的是里面有一个listeners,这是一个监听器功能。
  • 然后是OnAfterFormLoad()方法。父类里面并没有定义内容,只是可以被继承重写。
  • 之后是建立调用OnCreateEditWin()方法编辑页面。
  • var pleditview这个画的看图说话吧,语言有时候并没有图好。(我这里直接修改me.tabMain里面的items,同时删掉plGrid情况下的对比图)

pleditview

pleditview

  • me.tabMain其实对应上图无pleditview的空白面板
  • me.tabMain.getTabBar().setVisible(false)这一步注释掉页面会变得很难看。字面理解是把TabBar隐藏掉,实际上,按钮依然出现但是。。。看图吧

  • mainpanel.add(me.tabMain)这一步其实是把页面加到整个系统界面上,不然点击目录没反应。
  • 有时候,我们在列表页点击新增时某些自断是不给填写设置的默认值,这时候需要我们紧接着调用me.OnLoadData(record);以及me.OnInitData();两个方法,不能变换位置,否则发现页面上没有默认值。

接下来看看OnLoadData()方法
返回目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// busform.js
OnLoadData: function (record) {
var me = this;
var mep = me.tranPrefix;
if (!Ext.isEmpty(eval('record.' + me.idPrevNext))) {
return me.OnSetData(eval('record.' + me.idPrevNext), record);
} else {
me.dataDeal == gpersist.DATA_DEAL_SELECT;
tools.alert(Ext.String.format(gpersist.STR_FMT_NOLOAD, me.editInfo));
}
return false;
},
// 某继承者
OnLoadData:function(item){
var me = this;
var mep = me.tranPrefix;
// item 里的属性对应 T_Set_Sql_Column 表里设置的属性,如果发现补全就补全或者类似下面注释的方法重新调接口获取
// var item = tools.JsonGet(tools.GetUrl('接口?project.projectid=') + item.projectid);
// console.log(item)
var curDate = new Date()
//格式化;
var time=Ext.Date.format(curDate, 'Y-m-d H:i:s');
if (item && !Ext.isEmpty(item.projectid)) {
tools.SetValue(mep+'projectid',item.projectid);
tools.SetValue(mep+'projectname',item.projectname);

当双击某条数据去查看详情时会发现有些字段没有值,主要是因为item里的属性对应 T_Set_Sql_Column 表里设置的属性,如果发现补全就补全或者类似下面注释的方法重新调接口获取

附件相关学习

返回目录
      首先,附件相关是有通用代码的,在busform.js,待会详细学习,开发过程中把代码完全copy过来有时候并不能用,这时候我们自己必须要去慢慢找原因,所以还是理解下比较好。
      主要是从OnCreateEditWin()开始的,代码较多就不贴了。也用到了OnShowDetail(),OnListDeleteLeft(),OnListNewLeft(),OnCreateWinLeft(),OnDownLoad()OnCloseWin()以及lookpic()方法,但是父类并没有。据前辈们说,这里用了第三方插件。

  • OnBeforeCreateEditToolBar()OnAfterCreateEditToolBar()以及OnAfterCreateEdit()方法并没有什么内容
  • me.editToolItems定义了页面ToolBar上的按钮,然后通过tools.SetEditToolbar()控制权限对按钮进行增删保留。
  • me.tbEdit创建Toolbar一栏(面板)
  • 之后调用me.OnBeforeCreateEdit()方法建立编辑页面内容
  • 这里的me.plEdit我给他注释掉发现控制台报Cannot read property 'getForm' of null,且主界面没有了,附件页面放大占领了主界面原来位置。
  • 父类里紧接着判断me.hasDetail是否为true,true的话才会创建面板在页面上显示。我试着把继承者hasDetail属性设置为false,发现页面如图所示:

  • 经过tools.GetGridColumnsByUrl()方法,me.columnDetails从后台接收到数据已经不是空数组了。接下来的Ext.each()方法对列名数组进行遍历,判断是不是checkbox类型的列,如果是的话,给该列绑定一个beforecheck事件,然后给对应的checkbox改变选中状态。
  • me.cellEditing这里创建了cellEditing面板,clicksToEdit属性代表单击几次进入编辑,父类设置的是1,即单击一次即可进入编辑。
    listeners属性代表绑定监听事件。edit事件绑定了刷新;beforeedit事件绑定函数内部有一个判断,如果me.dataDeal == gpersist.DATA_DEAL_SELECT返回false或者调用me.OnBeforeDetailEdit(e, editor),然后看了下OnBeforeDetailEdit()直接就返回false没有任何操作。。。
    所以个人认为这里的beforeedit事件要么返回false要么没有返回值。
    我在继承者页面里面并没有找到该段代码,隐藏父类该段代码也没有明显变化,且页面无报错。

  • 父类接下来是me.plDetailGrid对象,该对象应该是一个模板,继承者页面命名可以与其不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 附件面板
me.plDetailGrid = Ext.create('Ext.grid.Panel', {
id : mep + 'detailgrid',
region : 'center',
title : me.detailTitle, // 附件面板标题
autoScroll : true, // 自动滚屏
frame : false, // 为true时边框为圆角且具有背景色
border : false,
margins : '0 0 0 0',
padding : '0 0 0 0',
loadMask : true, // 加载数据时为元素做出类似于遮罩的效果
columnLines : true, // 如果改为false看下图
viewConfig : {
autoFill : true, // 当行大小变化时始终填充满
stripeRows : true // 在表格中显示斑马线
},
features : [
{ ftype : 'summary' } // 百度了小,貌似是表格汇总属性
],
columns : me.columnDetails, // 列名
store : me.gridDetailStore, // 后台返回的数据
selModel: me.hasDetailGridSelect ? new Ext.selection.CheckboxModel({ injectCheckbox: 1, checkOnly: true }) : {},
plugins: plugins, // 可以没有,发现继承者页面就没有。。。应该是扩展插件的意思
listeners : {
'itemdblclick' : { fn : me.OnListSelect, scope : me } // 监听点击事件调用OnListSelect()方法
}
});
// busform.js
OnListSelect: function(e, record, item, index) {
var me = this;
me.detailRecord = record;
if (!me.winDetail) // 如果winDetail面板不存在,调用OnCreateDetailWin()方法
me.OnCreateDetailWin();
if(me.winDetail && record) { // 如果winDetail面板存在并且record也存在,显示winDetail面板
me.winDetail.show();
me.detailEditType = 2; // 2对应着修改
me.OnLoadDetailData(record.data); // 父类定义了该方法但没有内容
me.OnAuthDetailEditForm(true); // 这个方法管理页面上的按钮是否起作用
}
},
OnAuthDetailEditForm : function(islayout) {
var me = this;
var mep = this.tranPrefix;
if (islayout)
me.plDetailEdit.suspendLayouts(); // Ext 自带的禁用重绘功能。为了大数据量Grid做数据移除和添加而优化提高效率
switch (me.dataDeal) {
case gpersist.DATA_DEAL_SELECT:
tools.FormDisable(me.disDetailControls, me.enDetailControls, mep); // 禁止编辑输入框,只读
tools.BtnsDisable(['btnDetailSave'], mep); // 禁用按钮
break;
default:
tools.FormInit(me.disDetailControls, me.enDetailControls, mep);
tools.BtnsEnable(['btnDetailSave'], mep);
break;
}
if (islayout) {
me.plDetailEdit.resumeLayouts(true); // Ext 自带的开启重绘功能
me.plDetailEdit.doLayout(); // Ext 自带的重绘
}
},

      看了下OnListSelect()方法,父类的意思应该是当点击附件列表时进入详情页可以编辑修改。
      不过我看了继承者页面,发现它并没有使用该方法反而重写了一个方法lookpic(),用来点击预览的。据徐冬冬说,这个用了第三方插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
lookpic: function (view, record) {
项目名.PopupFileShow('预约附件预览', '接口', record.data.attachurl, record.data.attachname);
},
// tools.js
tools.GetPopupWindow = function (namespace, cname, config) {
var classname = namespace + '.' + cname;
var win;
if (eval(classname)) {
win = Ext.create(classname, config);
}
else {
Ext.Ajax.request({
url: '地址' + cname + '.js',
async: false,
method : 'GET',
success: function (response, opts) {
eval(response.responseText);
},
failure: function (response) { }
});
win = Ext.create(classname, config);
}
return win;
};

PopupFileShow()方法里调用了tools.GetPopupWindow()方法返回了Ext对象并且调用OnSetData()方法。

  • me.deitems定义了me.plDetailGrid面板按钮,然后通过me.plDetailGrid.insertDocked(0, tbdetailedit)把刚刚定义的按钮添加到me.plDetailGrid面板里。然后判断me.hasPageDetail是否为true,是的话在列表页面底部插入分页。
    这里我感觉很奇怪,明明在OnFormLoad()里有分页控制了,这里为什么还需要呢?看了下继承者页面并没有该段代码,注释掉父类也没报错,页面显示正常。
  • detailTabs属性只在busform.js里面找到,默认值是1,查了下可能是控制tab页打开数量的。然后看了下继承者页面,直接没要这个判断,然后把所有面板放到me.plDetail.items属性里,这样页面上就有多个附件面板展示了。看下图:

附上上传附件通用代码

返回目录

贴上主要的附件代码,防止忘记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// 点击上传时调用
OnShowDetail: function(render, item){
var me = this;
var mep = me.tranPrefix;
// console.log('1010101010,OnShowDetail');
console.log(me.plCheckGrid.store.getAt(0));
console.log(item);
var newreord = me.plCheckGrid.store.model.create({});
newreord.data.attachname = item.name;
newreord.data.attachurl = item.url;
//判断是否是图片
if('.jpg'==item.type||'.png'==item.type||'.gif'==item.type){
newreord.data.attachtypename='图片';
newreord.data.attachtype='1';
}else{
newreord.data.attachtypename='文件';
newreord.data.attachtype='2';
}
me.plCheckGrid.store.insert(me.plCheckGrid.store.getCount(), newreord);
me.plCheckGrid.getView().refresh();
},
//关闭图片上传窗口 时调用
OnCloseWin: function(){
var me = this;
var mep = me.tranPrefix;
// console.log('1212121212,OnCloseWin')
me.winLeft.destroy();
},
//图片文件查看 双击文件查看时调用
lookpic: function (view, record) {
// console.log('1313131313,lookpic')
console.log(record.data)
gmo.PopupFileShow('预约附件预览', '接口', record.data.attachurl, record.data.attachname);
},
// 点击下载时调用
OnDownLoad:function(){
var me = this;
var mep = me.tranPrefix;
var record = tools.OnGridLoad(me.plCheckGrid, '请选择需要下载的文件!');
var url = tools.GetEncode(tools.GetEncode(record.attachurl));
console.log(url);
var fileurl = tools.DoAction(tools.GetUrl('接口?fileurl=') + url);
// console.log('url: ' + url)
// console.log(fileurl)
// console.log('1414141414,OnDownLoad')
if(fileurl == 'error'){
tools.alert('当前文件不存在');
return;
}
window.location.href = tools.GetUrl('接口?fname=') + fileurl;
},
// 点击删除按钮时调用
OnListDeleteLeft: function () {
var me = this;
// console.log('1515151515,OnListDeleteLeft')
console.log(me.plCheckGrid.selModel.selected.items[0])
var j = me.plCheckGrid.selModel.selected.items.length;
for (var i = 0; i < j; i++) {
me.plCheckGrid.store.remove(me.plCheckGrid.selModel.selected.items[0]);
}
me.plCheckGrid.getView().refresh();
},
// 文件选择器 点击新增时调用,在 OnCreateWinLeft 之前
OnListNewLeft: function () {
var me = this;
// console.log('1616161616,OnListNewLeft')
me.OnCreateWinLeft();
if (me.winLeft) {
me.winLeft.show();
}
},
// 点击新增时调用,在 OnListNewLeft 之后
OnCreateWinLeft: function () {
var me = this;
var mep = me.tranPrefix;
// console.log('17171717,OnCreateWinLeft')
if (Ext.getCmp(mep + 'editleft')) {
Ext.getCmp(mep + 'editleft').destroy();
}
var items = [
' ', { id: mep + 'btnLeftSave', text: gpersist.STR_BTN_SAVE, iconCls: 'icon-save', handler: me.OnSaveLeft, scope: me },
'-', ' ', { id: mep + 'btnLeftClose', text: gpersist.STR_BTN_CLOSE, iconCls: 'icon-close', handler: function () { me.winLeft.hide(); } }
];
var edits = [
{xtype: 'container', anchor: '100%', layout: 'column', items: [
{xtype: 'container', columnWidth: .5, layout: 'anchor', items: [
tools.FormText('附件名称', 'ifn.filetitle', mep + 'filetitle', 20, '96%', false, 1)
]},
{xtype: 'container', columnWidth: .5, layout: 'anchor', items: [
tools.UploadFile('上传图片', 'image', mep+'fileurl',100, '100%', true,1)
]}
]}
];
me.plEditLeft = Ext.create('Ext.form.Panel', {
id: mep + 'pleditleft',
columnWidth: 1,
fieldDefaults: {
labelSeparator: ':',
labelWidth: gpersist.CON_DEFAULT_LABELWIDTH,
labelPad: 0,
labelStyle: 'font-weight:bold;'
},
frame: true,
title: '',
bodyStyle: 'padding:5px;background:#FFFFFF',
defaultType: 'textfield',
closable: false,
header: false,
unstyled: true,
scope: me,
tbar: Ext.create('Ext.toolbar.Toolbar', { items: items }),
items: edits
});
var upload = tools.SwfUploadFile();
me.winLeft = Ext.create('Ext.Window', {
id: mep + 'editleft',
title: '添加项目附件',
width: 680,
height: 160,
maximizable: true,
closeAction: 'hide',
modal: true,
layout: 'fit',
plain: false,
closable: true,
// items: [me.plEditLeft]
items: [upload]
});
upload.on('showdetail',me.OnShowDetail,me);
upload.on('closewin',me.OnCloseWin,me);
},
...

四、踩坑集锦

返回目录
(1).写项目申报时会有附件上传以及预览、下载功能。该功能代码其实直接复制粘贴过来就好了,但是,并不代表直接能用。

  • 我一开始复制过来时,进行文件上传发现报谷歌浏览器flash版本太低,没事,换360即可。
  • 点击上传文件,功能正常,但是我发现列表没有展示。后来发现是OnCreateEditWin()方法里面的urlcolumncaskask不对,对应的sqlid没有。

  • 当我点击下载按钮时一直报错(当前文件不存在),追踪到OnDownLoad()方法,发现
    var fileurl = tools.DoAction(tools.GetUrl('Upload接口?fileurl=') + url);返回的是error。然后只能去tools.js里面找GetUrl()DoAction()方法。GetUrl()只是返回了url,没做出什么改变; DoAction()方法其实是利用ajax调接口,也没有做什么改动,那只能说明接口调用失败。
          期间把变量打印出来,然后在本地看,发现图片并没有保存在该项目内,反而自动新建了个文件夹并保存在新建的文件夹内,很奇怪,问了同事说这是两个项目共用的,都要能看到,然后就没管。但是一直找不到什么原因,当然对框架也不熟。
          后来陈老师继续出马,发现我图片保存地址不在该项目内,然后项目内全局搜索图片保存的文件夹名(即另一个项目名例如a),发现有几处a,然后把gpersist.properties里面的FILE_PATH_BASE值改为当前项目名,结果可以用了。
          但是,同事说不能这样该配置,不然的话另一个项目就看不到你上传的东西了。。。这就尴尬了。
          后来陈老师继续出山,帮我看了下,发现UpdateToProject()接口内PATH_GET_IMAGE_WINDOWS路径不对。改为保存附件的文件夹名即可。

  • 然后就是双击点开预览的坑了。发现一直在加载但就是不出来,控制台反应调接口报错。请了老员工帮我看,看了快一小时都不知道为什么,
          后来同事说预览的话需要本地有个插件,然后给了我LibreOffice 5文件夹,但是也没说放在哪,我就放到项目里面结果依然没用。实在没办法了,只好再请陈老师出山,当然,我也问了他是否需要LibreOffice 5以及放的位置对不对。然后继续打开配置文件,发现

          这就真的很尴尬了,原来需要两个插件而且路径也得放对。。。23333333333。然后,解决。。。这坑真的很烦,还浪费时间!

(2).Could not find action or result;No result defined for action报错。
返回目录
      依然是个很尴尬的问题。由于java类里面某个字段是double类型,但是我在测试时投机取巧直接复制粘贴,所有数据弄一样的,结果传到后台报错。。。一头雾水,发现测试组测试时并没有这个错,这就很奇怪了。
      然后打了断点发现没进去,页面报404,只好请了同事帮我一起看,两人琢磨了一会然后跟着报错信息找到配置文件gmo.xml,他看了下说会不会是你返回的类型不对或者传入的类型不对,这么一说我立马在页面上看,智障,有个字段是double类型的我传的是string类型,不错才怪。

(3).下拉选择框
返回目录

      tools.ComboStore('example', gpersist.SELECT_MUST_VALUE),这里的example对应T_Set_Select表里的sqlid,其中sqltext对应select语句,且里面至少两个字段,例如:

1
select id,name from 表

但是,在页面上select框展示的永远只有最后一个字段,且向后台传值的话传第一个字段

(4).保存时主键提示语修改
返回目录
      测试组发现新增时,有的编号是主键并且是String类型的,没法自增,只能新增时填写。但是,他们故意新增了个已经存在的编号结果报主键冲突,这样肯定是不好的。
      我一开始的想法是在后台保存前先调用search方法查看表里已有的数据,然后通过循环遍历来判断目前新增的编号是不是已经存在,如果存在就不保存给提示。但是由于我java并不擅长,写得时候接收search方法返回的参数时就出现了问题,遂问黄健雄。然后他立马制止了我这么干,因为直接在action层调方法有风险,尤其是search方法打印输出流会对save方法造成影响,然后他提供了个思路即找到报错类型加个判断改变提示语,这个方法真的很巧妙!
      后来我追踪打印了报主键冲突作物的状态码,然后通过判断改变提示语。

(5).新增时发现数据默认-2以及me.disNews
返回目录
      第一次写页面时纯复制粘贴,当时留了个坑,如图:

      点击新增时居然出现-2,虽然能正常编辑保存,但是这样肯定是不美观的。直到最近写过三次并且有时间折腾了才发现,原来是OnInitData()方法里面的问题。初始化赋值时可能后台并没有返回值所以给了个默认值-2。
      还有另一个问题是me.disNews,me.disEdits,me.enNews,me.enEdits

1
2
3
4
me.disNews = ['a'];
me.disEdits = ['a'];
me.enNews = ['b'];
me.enEdits = ['b'];

      一开始我如上面代码所写,结果页面是这样的,

      这样肯定不好,我需要两个都能编辑,所以我全放开,改成这样:

1
2
3
4
// me.disNews = ['a'];
// me.disEdits = ['a'];
me.enNews = ['a','b'];
me.enEdits = ['b'];

结果:

很明显这样是不对的,应该改成:

1
2
3
4
// me.disNews = ['a'];
// me.disEdits = ['a'];
me.enNews = ['a','b'];
me.enEdits = ['a','b'];

(6).双击列表跳转页面查看详情时发现有些字段没有值
返回目录


      如上图所示,查看详情时发现试验地点字段没有值,但是在列表页search接口返回了,那为什么会少值呢?
      我查看浏览器打印台,发现并没有调其他接口,这就很奇怪了,值怎么来的?
      后来我发现,原来是通过OnLoadData()方法进行赋值的。我一开始猜测可能是先通过search接口获取到后台传过来的数据然后进行赋值,可是当我把后台传过来的所有数据都给它赋值上去后,页面上仍然没有值,明明接口里有返回的数据,但是为什么查看详情页会没有值呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
OnLoadData:function(item){
var me = this;
var mep = me.tranPrefix;
// item 里的属性对应 T_Set_Sql_Column 表里设置的属性,如果发现不全就补全或者类似下面注释的方法重新调接口获取
// var item = tools.JsonGet(tools.GetUrl('Get接口?trmproject.projectid=') + item.projectid);
// console.log(item)
var curDate = new Date()
//格式化;
var time=Ext.Date.format(curDate, 'Y-m-d H:i:s');
if (item && !Ext.isEmpty(item.projectid)) {
tools.SetValue(mep+'projectid',item.projectid);
...
},

      我试着打印item,发现里面的数据很少,列表页展示了多少它就多少,我很奇怪,why?后来问了陈老师,才知道:item里的属性对应 T_Set_Sql_Column 表里设置的属性,如果发现不全就补全或者类似上面注释的方法重新调接口获取,该item的属性其实对应着列表页的各个列名(当然,只要表里有对应的字段即使隐藏了也有对应的值。)

(7).从父类复制过来的按钮没有显示
      可能id冲突了,id名字改下。

(8).权限问题
      在开发过程中,会发现action层方法开头都有Menus = "101"之类的,一开始没怎么关注过,同事们一般也没管直接复制粘贴没改过。但是我作死改过,发现页面无数据,点开浏览器NetWork,返回值是[],打断点也没没进去,当然折腾了好久。
      然后找了黄健雄帮忙,黄健雄也不知道什么原因,但是看到返回值是[],说记得拦截器那边会返回[]
      既然如此,便试着在拦截器打了个断点,进去了,发现果然有个循环遍历menus(即该用户的权限值数组)。恰好我写的那个方法开头的Menus值不在该数组内,所以调用不了我写的那个方法。
      这个坑真的熊都没遇到过,因为都是复制粘贴所以从没遇到这个坑,也算长见识了。

(9).数据中存在空格、换行等页面展示/n字符问题
返回目录
      测试在输入时使用了换行符,结果存到数据库里面去了,取得时候就发现后面带个/n,页面上展示出来很丑。后来发现前台tools.js里面有针对该情况的解决方法即tools.ReplaceValue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tools.ReplaceValue = function (str) {
var result = "";
if (str){
var en = new RegExp('///n','g');
var er = new RegExp('///r','g');
var et = new RegExp('///t','g');
var el = new RegExp('“','g');
var eg = new RegExp('”','g');
result = str.replace(en,'\n').replace(er,'\r').replace(et,'\t').replace(el,'"').replace(eg,'"')
return result;
}
else
return result;
};

      补充说明:在后期测试时发现列表面板里依然出现///n这种符号,但详情页没有。这时候一个解决思路是找到列表对应的search接口手动把数据replaceAll掉。

(10).进入详情页控制台报cannot read property 'getLeft' of undefined问题
返回目录
      点击进入详情页,带附件面板的。发现控制台报错,如下图。

      点开发现是ext-all-debug.js里面的方法报错,然后我找阿找,愣是没找到this.el是什么。实在没办法我在页面上试。一开始发现如果没有附件没有报错,所以在想是不是附件加载的问题,但是后来发现即使没有附件也会报错,只是不立即报错,当时想难道是定时器?但是页面上也没有啊。

1
2
3
isOnLeftEdge: function(e) {
return (e.getXY()[0] - this.el.getLeft() <= this.handleWidth);
},

      偶然间,真的是偶然间发现,鼠标移动到附件面板时出现了报错,我当时就操了。然后看附件面板代码,也没看出什么不同。后来仔细移动鼠标,发现移动到附件面板列名时报错!!!amazing!!!
      这下大概知道了是面板列名的错,我把var urlcolumncaskask注释掉,果然不报错,但是页面上也没有列名了,这样肯定不行的。然后我仔细对比发现,下方多了一段一模一样的代码,然后注释掉,艹,没问题了。fuck!!!
      这明显是代码复制粘贴时没有复制全或者没有删全造成的,不过如果仅仅复制粘贴的话,的确不知道该代码有什么用。所以还是要理解下的。
我多出来的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// me.detailFuncs = Ext.create('Ext.util.MixedCollection');
// me.columnCaseAsk = [];
// me.fieldCaseAsk = [];
// me.OnGetDetailFunction();
// var urlcolumncaskask = 'SysSqlColumn.do?sqlid=p_get_trmrulesattach';
// var urlcaseaskstore = 'TrmSearchTrmRulesAttach.do';
// tools.GetGridColumnsByUrl(urlcolumncaskask, me.columnCaseAsk, me.fieldCaseAsk, mep + '_sh_', me.detailFuncs, me.hasDetailCheck);
// me.gridSaleHighStore = tools.CreateGridStore(tools.GetUrl(this.urlcaseaskstore), me.fieldCaseAsk);
// Ext.each(me.columnCaseAsk, function (col, index) {
// if (col.xtype == 'checkcolumn') {
// col.on('beforecheck', function(check) {
// if (me.dataDeal == gpersist.DATA_DEAL_SELECT)
// check.isCancel = true;
// else
// check.isCancel = false;
// });
// };
// });
// me.OnBeforeCreateDetail();

(11).详情页有附件面板,新增时,附件面板默认出现某条数据的问题
返回目录
      测试发现,带有附件面板的详情编辑页,在新增时会默认有一条附件数据,这是不对的。如图:

      很明显,新增时刚进入页面,图上的附件面板附件编号22那条数据不应该出现。我看了下NetWork,发现一进页面就掉接口得到返回值了。其实如果是双击查看详情页或者修改的情况下这样是没问题的,但是可惜新增时也调了同样的方法,开发过程中忽略了新增时这个情况,所以需要做个判断。如果是新增的情况下,就不给它调接口。
      但是,该判断怎么加呢?加在哪呢?
      我发现该接口分别在OnDetailRefresh()方法以及OnCreateEditWin()里面调用。我先是注释掉OnCreateEditWin()里面的接口,没用;然后注释掉OnDetailRefresh()里面的接口,有用。但是双击查看时附件也不见了,说明需要在OnDetailRefresh()里加个判断。
      但是我们需要知道它点新增时调用的是什么方法,点开父类找到OnNew()方法,发现它里面调用了OnInitData()方法,在OnInitData()里发现又调用了OnDetailRefresh()方法。
      好像很明了了,但是回到子类看完全不是啊,子类把OnInitData()方法重写了,子类里面的OnInitData()完全没有调用OnDetailRefresh(),反而是在OnLoadData()里面调用的。
      一开始我以为可以通过传递idOnDetailRefresh()方法,然后没有id就不调接口。确实实现了,但是出现了另一个问题即当我先点击其他带有附件的记录并进入详情页查看后,然后点击新增时发现附件面板依然有数据,恰好是刚刚点击查看详情的那条记录的附件信息,很明显,这依然不对。然后我发现NetWork并没有调接口,那只能说明是先前加入面板的数据没有清空,所以我百度了下找到了清空方法,在调接口前先使用me.plCheckGrid.store.removeAll()清空面板就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// OnLoadData()方法
me.OnDetailRefresh(id);
// OnDetailRefresh()方法
OnDetailRefresh : function(itemId) {
var me = this;
var mep = me.tranPrefix;
var id = tools.GetValue(mep+'id');
var ide = tools.GetEncode(tools.GetEncode(id));
me.plCheckGrid.store.removeAll();
if (me.plCheckGrid && me.plCheckGrid.store && id) {
me.plCheckGrid.store.proxy.url = tools.GetUrl('接口.id=') + id;
me.plCheckGrid.store.load();
}
}

(12)下拉框验证问题
返回目录
      测试给熊提了个bug,说下拉框没有选择默认’请选择’字段点击保存居然也可以,存到数据库中值为-2,这时很明显需要加个验证提示。打开tools.js里面的tools.FormCombo()方法,发现只有不为空的验证,其他什么验证也没有。后来熊在网上找到了Ext中的combox以及里面自带的属性,其中有一个函数validator()可以用来进行其他验证。
      添加的代码如下:

1
2
3
4
5
6
7
validator: function(value) {
if(value == '--请选择--') {
return '请选择' + label + '!';
} else {
return true;
}
}

(12)部分word 2007/2010 不能预览问题
返回目录
发现word 2007/2010版本的无法预览,后台报错org.artofsolving.jodconverter.office.OfficeException,百度搜了下可能是插件兼容性的问题,目前无解。。。
黄健雄找了个折中的方法,在接口内部加一个ReturnValue,然后通过判断ReturnValue来改写提示语。

好吧,最终解决了。感谢陈德林陈老师,他之前也发现过 word 2007 以上版本有问题,然后他改了下代码,好吧,我看不懂也改不了。实力太菜。。。

FileUpload里面的copy方法修改:

1
2
3
4
5
6
7
8
9
int numBytesRead = 0;
// 一次写入多个字节到输出流中,减少IO访问次数
while((numBytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, numBytesRead);
}
// 多次写字节到输出流中,在office 2007 版本以上会报错
// while (in.read(buffer) > 0) {
// out.write(buffer);
// }

Newer Post

js函数作用域和块作用域

学习自《你不知道的javascript》一、函数作用域1.1 规避冲突12345678910function foo() &#123; function bar(a) &#123; i = 3; // 修改for循环中的i …

继续阅读
Older Post

js词法作用域----静态作用域

学习自《你不知道的javascript》和:https://github.com/mqyqingfeng/Blog/issues/3js采用词法作用域(lexical scoping),也就是静态作用域!!! 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。即编写代码的时候作用域就定义好 …

继续阅读