为了提升性能与用户体验,可以列表界面加载数据时,可以采用ajax方式。
在 BroFramework 中,通过查询框架,可以很轻松的实现 ajax 加载的需求,示例如下。
- 修改 list.gsp,给 s:form 标签添加属性 ajax,如:
<s:form action="${actionName}" ajax="true"> - 将 list.gsp 中的 <div class="list"> 和 <div class="paginateButtons">两端代码,剪切出来粘贴到一个新的 _list.gsp 文件中
- 修改 list.gsp ,在上述两个 div 原本所在位置插入标签 g:render,如:
<g:render template="/foo/bar/list" plugin="bro-foobar" />
注:如果是插件开发,则需要 plugin 属性,否则不需要 - 修改控制器中的 list 方法,将最后的 render 修改为:
if ( isAjax() ) {
render ( view:"${VIEW_PATH}_list", model:[......] )
} else {
render ( view:"${VIEW_PATH}list", model:[......] )
}
也就是判断 ajax 请求时,仅渲染 _list.gsp
经过上述改造后,页面加载的时候会一次性渲染完整的第一页,但是点击翻页、表头排序、查询时,都会自动采用 ajax 方式加载列表了。
此外,某些场景下,列表页面中需要加载多个页签,并且每个页签下显示一个独立的列表,但是页签顶部有一个统一的查询栏,效果如下图所示:

这个需求开发略微复杂,以上述截图中的待办列表为例。
改造 list.gsp,添加页签:
<%-- s:form 标签添加 ajax 属性--%>
<s:form action="${actionName}" ajax="true"><s:setup/>
<div class="body">
<h1><g:message code="bropen.workbench.task.Todo"/></h1>
<g:if test="${flash.message}"><div class="message">${flash.message}</div></g:if>
<g:if test="${flash.errors?.hasErrors()}"><div class="errors"><g:renderErrors bean="${flash.errors}" as="list" /></div></g:if>
<%-- 唯一的搜索栏 --%>
<s:filter>
<div class="right">
....
</div>
</s:filter>
<%-- 生成页签 --%>
<script>$j(function(){ $j("#tabs").tabs() })</script>
<div id="tabs">
<ul>
<g:each in="${lists}" var="m" status="i">
<li><a href="#tab-${i}">${m.name}(${m.total})</a></li>
</g:each>
<%-- 创建两个右对齐的按钮型页签 --%>
<li class="right refresh" style="cursor: pointer" onclick="reloadTasks()"> </li>
<li class="right oarequirement" style="cursor: pointer" onclick="xx"> </li>
</ul>
<%-- 遍历多个页签,生成列表及其页签容器,注意容器设置了一个css class,生成列表时,model中增加了一个参数 tabId 用于标识哪个页签、并用于控制器判断加载更新哪个页签的数据 --%>
<g:each in="${lists}" var="m" status="i">
<div id="tab-${i}" class="tabId${m.tabId}">
<g:render template="/foo/bar/list" plugin="bro-foobar" model="[list: m.list, total: m.total, tabId: m.tabId]" />
</div>
</g:each>
</div>
</div>
</s:form>
子列表页面 _list.gsp 如下:
<div class="list">
<table class="fixedEllipsis">
<%-- 设置表头,注意参数 tabId --%>
<thead>
<tr>
<g:sortableColumn params="[tabId: tabId]" property="security" title="${message(code:'bropen.workbench.task.Task.security')}" width="30" />
......
</tr>
</thead>
<tbody>
<g:each in="${list}" status="i" var="task">
<tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
<td><a href="${task.url}" target="_blank">${task.docNumber}</a></td>
......
</tr>
</g:each>
<g:each in="${(total != null && setting('bropen.framework.pagination.default')-list.size()) ? (list.size()..setting('bropen.framework.pagination.default')-1) : null}" var="i">
<tr class="${(i % 2) == 0 ? 'odd' : 'even'}">${"<td> </td>".multiply(10)}</tr>
</g:each>
</tbody>
</table>
</div>
<%-- 设置翻页栏,注意参数 tabId --%>
<g:if test="${total}"><div class="paginateButtons"><g:paginate total="${total}" params="[tabId: tabId]" /></div></g:if>
控制器操作如下:
// ajax 加载一个页签
if ( params.tabId ) {
def list = ...
def total = ...
return render ( view:"${VIEW_PATH}_list", model:[list: list, total: total, tabId: params.tabId] )
}
// 第一次加载或搜索,计算多个页签
else {
def lists = []
for ( x in xx ) {
lists << [tabId: 标签标识, name: 标签名,list: 列表数据, total: N]
}
render ( view:"${VIEW_PATH}list", model:[lists: lists] )
}
这样改造后,任意一个列表翻页、排序时,会覆盖其他列表,因此,每次翻页或排序时,需要指定本次操作的容器(某个页签),这里通过查询框架的事件来实现;此外,由于只有一个查询栏,点搜索按钮时也会自动用 ajax 方式查询,得到的结果也不是想要的,需要改造成搜索时不通过 ajax、直接加载整个页面,此时同样可以通过查询框架的事件来执行。如下面的 js 示例代码,贴到 list.gsp 中:
1、在页签中翻页、排序时(我们在 _list.gsp 中设置了一个 tabId 的额外参数,通过它可以判断),给 s:form 设置一个 ajaxContainer 的表单参数,指向 list.gsp 中的页签 div 容器
2、搜索操作时,删除 s:form 标签的 ajax 属性,实现非 ajax 查询
/**
* 多页签时,用 ajax 方式提交翻页、排序,非ajax方式提交搜索
*/
search.callbackBeforeSubmit = function( actionUrl ) {
var form = $j(search.form());
// 如果是多页签
if ( form.attr("ajax") && $j("#tabs").length ) {
if ( actionUrl.indexOf("tabId") > 0 ) {
// 翻页、排序时,设置列表容器
var container = actionUrl.replace(/.+(tabId=[^&]+)(&.+)?/, "$1").replace("=", "");
form.attr("ajaxContainer", "." + container);
} else {
// 点搜索按钮时,取消ajax
form.removeAttr("ajax");
}
}
}
最后,默认的 jQuery UI 的页签组件显示效果适用于表单,在列表界面中不太好看,因此,需要设置一些 css 样式,上图中的样式示例如下:
/** 页签高宽 */
.ui-tabs .ui-tabs-panel {
padding: 0px;
}
.ui-tabs .ui-tabs-nav {
padding-top: 0px;
}
.ui-tabs .ui-tabs-nav li,
.ui-tabs .ui-tabs-nav li.ui-tabs-active {
cursor: pointer;
border: none;
margin-right: 15px;
margin-bottom: 2px;
padding-bottom: 1px;
text-align: center; /** 文字居中 */
height: 20px; /** 和图片高宽吻合 */
width: 99px;
}
.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
float: none; /** 文字居中 */
}
/** 页签背景 */
.ui-tabs .ui-corner-top {
border-top-right-radius: 0px;
border-top-left-radius: 0px;
}
.ui-tabs .ui-widget-header {
background: url("tab-background.gif") repeat-x;
border: none;
}
.ui-tabs .ui-state-default {
background: url("tab.jpg") repeat-x scroll 50% 50% #ffffff;
}
.ui-tabs .ui-state-active {
background: url("tab-active.jpg") repeat-x scroll 50% 50% #f1f1f1;
}
/** 页签字体 */
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited {
color: #333;
line-height: 2;
}
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited {
color: #06C;
line-height: 2;
font-weight: bold;
}
/** 右侧页签 */
.ui-tabs .ui-tabs-nav li.right {
float: right;
margin-right: 0px;
margin-left: 15px;
}
/** 右侧页签按钮示例 */
.ui-tabs .ui-tabs-nav li.oarequirement {
background: url("sample/oarequirement.gif") no-repeat scroll 50% 50% #ffffff;
width: 63px;
}
.ui-tabs .ui-tabs-nav li.refresh {
background: url("sample/refresh.gif") no-repeat scroll 50% 50% #ffffff;
width: 63px;
}
/** 搜索栏 */
.searchfilter {
border: none;
}