如何开发 AJAX 列表（list）页面、如何开发多页签的列表页面?
李小翔
2014-11-19 18:04

内容列表

为了提升性能与用户体验，可以列表界面加载数据时，可以采用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()”>&nbsp;</li>
               <li class=”right oarequirement” style=”cursor: pointer” onclick=”xx”>&nbsp;</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>&nbsp;</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;
}