JS实现二级列表(包含下拉刷新、上拉加载更多、侧滑操作等)

本篇文章讲述的是使用js实现可收缩二级列表,包含以下特性:

1,点击打开/收缩二级列表;

2,列表下拉刷新数据;

3,列表上拉加载更多数据;

4,列表item项可侧滑操作(类似于QQ和微信);

5,数据缓存;

先上效果图:


进入正题,下面是具体实践:

功能实现使用到了Framework7框架,Framework7官网地址为:http://framework7.taobao.org

后面我会上传完整demo(包含Framework7库,可直接使用)。


一,准备工作


1,引入Framework7库;

引入Framework7的详细教程我就不写了,可以直接看官网教程:http://framework7.taobao.org/get-started/#.WO8wgVK75bU。也可以直接下载这篇文章的demo,demo里已引入所有文件并做好了所有配置,可供参考。


二,具体实现


依旧简单粗暴贴代码吧。下面是完整代码文件结构:


以上图示文件中,主要代码有:monitor_main.js、accordion.js、monitor_main.html、monitor_main_tpl.html、monitor_main.css,其余均为Framework7库文件或配置文件。下面贴出主要代码:

1,页面布局monitor_main.html

[html]  view plain  copy
  1. <div class="pages navbar-through">  
  2.     <div data-page="monitor_main" class="page">  
  3.         <div class="page-header" style="position: absolute;z-index:9">  
  4.             <div class="page-header-inner">  
  5.                 <div class="left">  
  6.                     <a href="#" class="link">  
  7.                         <img src="images/monitor/menu.png" class="nav_open_panel">  
  8.                     </a>  
  9.                 </div>  
  10.                 <div class="center">JS二级列表</div>  
  11.                 <div class="right">  
  12.                     <a href="#"></a>  
  13.                 </div>  
  14.             </div>  
  15.         </div>  
  16.         <div style="width:100%; margin-top:0px; height:100%;" id="monitor_areas_list"  
  17.              class="page-content pull-to-refresh-content infinite-scroll" data-ptr-distance="55" data-distance="1">  
  18.   
  19.             <div class="pull-to-refresh-layer">  
  20.                 <div class="preloader cus-preloader"></div>  
  21.                 <div class="pull-to-refresh-arrow"></div>  
  22.             </div>  
  23.   
  24.             <div class="list-block cusv1-accordion-list" style="margin: 48px 0">  
  25.                 <ul id="areas_ul">  
  26.                 </ul>  
  27.             </div>  
  28.         </div>  
  29.     </div>  
  30. </div>  

2,列表使用模板 monitor_main_tpl.html
[html]  view plain  copy
  1. <li class="cusv1-accordion-item area_accordion_item" data-area_id={{item_area_id}} data-area_index={{item_area_index}}>  
  2.     <a href="#" class="item-link item-content accordion-item-header" style="display:block">  
  3.         <div class="area_list_item_unit1">  
  4.             <span class="area_list_source">{{item_area_name}}</span>  
  5.             <span class="area_equip_num">{{item_equip_num}}</span>  
  6.             <img src="images/monitor/icon_arrow_to_right.png" class="icon_to_expand_area">  
  7.         </div>  
  8.         <div class="area_list_item_unit2">  
  9.             <img src="images/monitor/monitor_location_icon.png" class="loc_icon"/>  
  10.             <span class="equip_list_location" style="width:80%">{{item_area_location}}</span>  
  11.         </div>  
  12.     </a>  
  13.     <div class="accordion-item-content">  
  14.         <div class="list-block">  
  15.             <ul style="padding-left: 0">  
  16.                 {{#each subs}}  
  17.                 <li class="swipeout equip_according_list_item"  
  18.                     data-area_id={{area_id}} data-device_id={{device_id}} data-device_name={{device_name}}  
  19.                     data-isConcerned={{isConcerned}}>  
  20.                     <div class="swipeout-content">  
  21.                         <div class="concerns_click_area">  
  22.                             <img class="expanded_equips_item_state" src="{{concerns_state_icon}}"/>  
  23.                         </div>  
  24.                         <div class="expanded_equips_click_area">  
  25.                             <span class="expanded_equip_item_source">{{device_name}}</span>  
  26.                             <img src="images/monitor/icon_arrow_to_right.png" class="icon_to_equip_details">  
  27.                         </div>  
  28.                     </div>  
  29.                     <div class="swipeout-actions-right">  
  30.                         <a href="#" class="swipeout-close cancle_concerns equips_item_swipeout_btn"  
  31.                            style="background-color:{{btn_item_left_bg}}">{{btn_item_left_text}}</a>  
  32.                     </div>  
  33.                 </li>  
  34.                 {{/each}}  
  35.             </ul>  
  36.         </div>  
  37.     </div>  
  38. </li>  

3,收缩列表工具类 accordion.js

[javascript]  view plain  copy
  1. /*=============================================================================== 
  2.  ************   Accordion   ************ 
  3.  ===============================================================================*/  
  4. (function () {  
  5.     var $ = $$, app = myApp;  
  6.   
  7.     app.accordionOpenv1 = function (item) {  
  8.         item = $(item);  
  9.         var list = item.parents('.cusv1-accordion-list').eq(0);  
  10.         var content = item.children('.accordion-item-content');  
  11.         if (content.length === 0) content = item.find('.accordion-item-content');  
  12.         var expandedItem = list.length > 0 && item.parent().children('.accordion-item-expanded');  
  13.         if (expandedItem.length > 0) {  
  14.             app.accordionClose(expandedItem);  
  15.         }  
  16.         content.css('height', content[0].scrollHeight + 'px').transitionEnd(function () {  
  17.             if (item.hasClass('accordion-item-expanded')) {  
  18.                 content.transition(0);  
  19.                 content.css('height''auto');  
  20.                 var clientLeft = content[0].clientLeft;  
  21.                 content.transition('');  
  22.                 item.trigger('opened');  
  23.             }  
  24.             /* else { 
  25.              content.css('height', ''); 
  26.              item.trigger('closed'); 
  27.              } */  
  28.         });  
  29.         item.trigger('open');  
  30.         item.addClass('accordion-item-expanded');  
  31.     };  
  32.     app.accordionClosev1 = function (item) {  
  33.         item = $(item);  
  34.         var content = item.children('.accordion-item-content');  
  35.         if (content.length === 0) content = item.find('.accordion-item-content');  
  36.         item.removeClass('accordion-item-expanded');  
  37.         content.transition(0);  
  38.         content.css('height', content[0].scrollHeight + 'px');  
  39.         // Relayout  
  40.         var clientLeft = content[0].clientLeft;  
  41.         // Close  
  42.         content.transition('');  
  43.         content.css('height''').transitionEnd(function () {  
  44.             /* if (item.hasClass('accordion-item-expanded')) { 
  45.              content.transition(0); 
  46.              content.css('height', 'auto'); 
  47.              var clientLeft = content[0].clientLeft; 
  48.              content.transition(''); 
  49.              item.trigger('opened'); 
  50.              } 
  51.              else {*/  
  52.             content.css('height''');  
  53.             item.trigger('closed');  
  54.             // }  
  55.         });  
  56.         item.trigger('close');  
  57.     };  
  58.   
  59. })();  

4,逻辑实现 monitor_main.js

[javascript]  view plain  copy
  1. /** 
  2.  * Created on 2016/12/18. 
  3.  */  
  4.   
  5. var $$ = Dom7;  
  6.   
  7. var myApp = new Framework7();  
  8.   
  9. var mainView = myApp.addView('.view-main');  
  10.   
  11. var utils = {  
  12.     post: function (data, sf, ef, p) {  
  13.         var url = utils.postUrl();  
  14.         $$.ajax({  
  15.             url: url,  
  16.             async: true,  
  17.             method: 'POST',  
  18.             contentType: 'text/plain',  
  19.             crossDomain: true,  
  20.             data: data,  
  21.             success: function (e) {  
  22.                 if ("function" == typeof sf)  
  23.                     sf(e, p);  
  24.             },  
  25.             error: function (e) {  
  26.                 console.log(e);  
  27.                 if ("function" == typeof ef)  
  28.                     ef(e, p);  
  29.             }  
  30.         });  
  31.     },  
  32.     get: function (url, sf, ef, p) {  
  33.         $$.ajax({  
  34.             url: url,  
  35.             async: true,  
  36.             method: 'GET',  
  37.             contentType: 'application/x-www-form-urlencoded',  
  38.             crossDomain: true,  
  39.             success: function (e) {  
  40.                 if ('function' == typeof sf)  
  41.                     sf(e, p);  
  42.             },  
  43.             error: function (e) {  
  44.                 if ('function' == typeof ef)  
  45.                     ef(e, p);  
  46.             }  
  47.         });  
  48.     }  
  49. }  
  50.   
  51. var monitorMain = function () {  
  52.     var page, refreshContent, infiniteScroll, isLoading = false, isAreasLoading = false, data  
  53.         , templateStr, template, distreeData, cacheDatas = {}, isOperated = false, swipeout_item_data = {};  
  54.   
  55.     /* 
  56.      * 获取模板文本 
  57.      * */  
  58.     $$.get("pages/monitor/monitor_main_tpl.html"function (data) {  
  59.         templateStr = data;  
  60.     });  
  61.   
  62.     /* 
  63.      * 加载监控页面 
  64.      * */  
  65.     mainView.router.load(  
  66.         {  
  67.             url: 'pages/monitor/monitor_main.html'  
  68.         }  
  69.     );  
  70.   
  71.     /* 
  72.      * 页面pageBeforeAnimation回调 
  73.      * */  
  74.     $$(document).on('pageBeforeAnimation'function (e) {  
  75.         var page = e.detail.page;  
  76.         if (page.name == "monitor_main") {  
  77.             beforeAnimal(page);  
  78.         }  
  79.     });  
  80.   
  81.     /* 
  82.      * 页面pageBeforeAnimation回调执行动作 
  83.      * */  
  84.     function beforeAnimal(page) {  
  85.         var container = $$(page.container)  
  86.         container.find('.navbar').removeClass('navbar-hidden');  
  87.         container.find('.pull-to-refresh-content').css({'margin-top''-44px''padding-bottom''44px'});  
  88.         container.find('.pull-to-refresh-layer').css({'margin-top''0'});  
  89.     }  
  90.   
  91.     /* 
  92.      * 页面pageInit回调 
  93.      * */  
  94.     $$(document).on('pageInit'function (e) {  
  95.         page = e.detail.page;  
  96.         if (page.name == "monitor_main") {  
  97.             init(page);  
  98.         }  
  99.     });  
  100.   
  101.     /* 
  102.      * 页面pageInit回调执行动作 
  103.      * */  
  104.     function init(p) {  
  105.         page = $$(p.container);  
  106.         refreshContent = page.find('.pull-to-refresh-content');  
  107.         infiniteScroll = page.find('.infinite-scroll');  
  108.         template = Template7.compile(templateStr);  
  109.         //查询数据  
  110.         beginQuery();  
  111.         //绑定事件  
  112.         bindEvent("on");  
  113.         //构建文本  
  114.         buildAreasBox(distreeData);  
  115.         // 缓存数据-保存数据到sessionStorage,缓存模块相关操作逻辑代码较多,此demo中已去除  
  116.         // sessionStorage.setItem("distreeData", JSON.stringify(distreeData));  
  117.     }  
  118.   
  119.     /* 
  120.      * 查询数据 
  121.      * 先从缓存获取数据,缓存数据为空时再网络请求/模拟数据 
  122.      * 缓存模块相关操作逻辑代码较多,因缓存相关非此demo的重点,此demo中已去除 
  123.      * */  
  124.     function beginQuery() {  
  125.         // distreeData = JSON.parse(sessionStorage.getItem("distreeData"));  
  126.         if (distreeData == null) {  
  127.             // getMonitorAreasInfo("refresh");  
  128.             //无网络环境请求数据,此处自行组装模拟数据  
  129.             assembleAreasData("refresh");  
  130.         } else {  
  131.             buildAreasBox(distreeData);  
  132.         }  
  133.     }  
  134.   
  135.     /* 
  136.      * 绑定/卸载事件 
  137.      * */  
  138.     function bindEvent(t) {  
  139.         var method = t == "on" ? t : "off";  
  140.         if (refreshContent) {  
  141.             //列表下拉刷新事件  
  142.             refreshContent[method]('refresh', refleshHandler);  
  143.         }  
  144.         if (infiniteScroll) {  
  145.             //列表上拉加载更多事件  
  146.             infiniteScroll[method]('infinite', loadMoreHandler);  
  147.         }  
  148.         if (page) {  
  149.             //二级列表打开/收缩事件  
  150.             page[method]('click'".accordion-item-header", accordionToggle);  
  151.         }  
  152.     }  
  153.   
  154.     /* 
  155.      * 构建列表文本 
  156.      * */  
  157.     function buildAreasBox(items) {  
  158.         data = items;  
  159.         var i, item, htmlstr = '', areaUl = page.find("#areas_ul");  
  160.         for (i = 0; i < items.length; i++) {  
  161.             item = items[i];  
  162.             htmlstr += template(item).trim();  
  163.         }  
  164.         //给列表插入文本  
  165.         areaUl.html(htmlstr);  
  166.     }  
  167.   
  168.     /* 
  169.      * 渲染 
  170.      * 非此demo的重点,此demo中已去除相关代码 
  171.      * */  
  172.     function render(items) {  
  173.         cacheDatas = items;  
  174.         for (var key in items) {  
  175.             var el = page.find(".data-" + key);  
  176.             el.text(items[key]);  
  177.         }  
  178.     }  
  179.   
  180.     /* 
  181.      * 查询数据 
  182.      * */  
  183.     function queryData() {  
  184.         if (isLoading) {  
  185.             resetState();  
  186.             return;  
  187.         }  
  188.         isLoading = true;  
  189.         // getMonitorAreasInfo("refresh");  
  190.         //无网络环境请求数据,此处自行组装模拟数据  
  191.         assembleAreasData("refresh");  
  192.     }  
  193.   
  194.     /* 
  195.      * 重置状态 
  196.      * */  
  197.     function resetState() {  
  198.         isLoading = false;  
  199.         myApp.pullToRefreshDone();  
  200.         myApp.hideIndicator();  
  201.     }  
  202.   
  203.     /* 
  204.      * destroy页面 
  205.      * */  
  206.     function destroyPage(p) {  
  207.         console.log("destroy warning main page");  
  208.         cacheDatas = {};  
  209.         bindEvent("off");  
  210.     }  
  211.   
  212.     /* 
  213.      * 模拟一级列表(区域)数据 
  214.      * */  
  215.     function assembleAreasData(type) {  
  216.         distreeData = [];  
  217.         for (var i = 0; i < 12; i++) {  
  218.             var areaId = '0_' + i;  
  219.             var subsData = simulateEquipDatas(areaId);  
  220.             distreeData[i] = {  
  221.                 item_area_index: i,  
  222.                 item_area_id: areaId,  
  223.                 item_area_name: '数控机床区' + i,  
  224.                 item_equip_num: subsData.length,  
  225.                 item_area_location: '华南地区/深圳分公司/' + i + '号厂房/中区',  
  226.                 subs: subsData  
  227.             }  
  228.         }  
  229.     }  
  230.   
  231.     /* 
  232.      * 模拟二级列表(设备)数据 
  233.      * */  
  234.     function simulateEquipDatas(areaId) {  
  235.         var equipsArr = [];  
  236.         for (var i = 0; i < 6; i++) {  
  237.             equipsArr[i] = {  
  238.                 concerns_state_icon: "images/monitor/icon_concerns_no.png",  
  239.                 isConcerned: false,  
  240.                 btn_item_left_bg: 'darkorange',  
  241.                 btn_item_left_text: '点击关注',  
  242.                 device_id: '0_1_' + i,  
  243.                 device_name: '数控切割机' + i,  
  244.                 area_id: areaId  
  245.             }  
  246.         }  
  247.         return equipsArr;  
  248.     }  
  249.   
  250.     /* 
  251.      * 网络请求一级列表(区域)数据 
  252.      * */  
  253.     function getMonitorAreasInfo(type) {  
  254.         var data = JSON.stringify('post_data');  
  255.         var params = {  
  256.             url: 'http://***',  
  257.             data: data  
  258.         }  
  259.         utils.post(params, function (e) {  
  260.             resetState();  
  261.             distreeData = [];  
  262.             // distreeData =  
  263.         }, function (e) {  
  264.             console.log("连接服务器失败,请检查网络");  
  265.             resetState();  
  266.         });  
  267.     }  
  268.   
  269.     /*  
  270.      * 网络请求二级列表(设备)数据  
  271.      * */  
  272.     function getEquipsInfoByAreaId(area_id, _callback) {  
  273.   
  274.         var data = {}  
  275.         data = JSON.stringify(data);  
  276.         var params = {  
  277.             url: 'http://***',  
  278.             data: data  
  279.         }  
  280.         utils.post(params, function (e) {  
  281.             var equipsArr = [];  
  282.             _callback(equipsArr);  
  283.         }, function (e) {  
  284.             console.log("连接服务器失败,请检查网络");  
  285.             resetState();  
  286.         });  
  287.     }  
  288.   
  289.     /*  
  290.      * 二级列表打开/收缩事件  
  291.      * */  
  292.     function accordionToggle(e) {  
  293.         var item = $$(e.target).parents(".cusv1-accordion-item");  
  294.         if (item.length === 0) return;  
  295.         if (item.hasClass('accordion-item-expanded')) {  
  296.             item.find(".icon_to_expand_area").attr("src""images/monitor/icon_arrow_to_right.png");  
  297.             myApp.accordionClosev1(item);  
  298.         } else {  
  299.             myApp.accordionOpenv1(item);  
  300.             item.find(".icon_to_expand_area").attr("src""images/monitor/icon_arrow_to_bottom.png");  
  301.         }  
  302.     }  
  303.   
  304.     /* 
  305.      * 刷新事件-列表下拉刷新 
  306.      * */  
  307.     function refleshHandler(e) {  
  308.         console.log("区域列表下拉刷新");  
  309.   
  310.         myApp.showIndicator();  
  311.   
  312.         queryData();  
  313.   
  314.         myApp.hideIndicator();  
  315.   
  316.         // 加载完毕需要重置  
  317.         myApp.pullToRefreshDone();  
  318.     }  
  319.   
  320.     /* 
  321.      * 加载事件-列表上拉加载更多 
  322.      * */  
  323.     function loadMoreHandler(e) {  
  324.         console.log("区域列表上拉加载更多");  
  325.   
  326.         // 如果正在加载,则退出  
  327.         if (isAreasLoading) return;  
  328.   
  329.         // 设置flag  
  330.         isAreasLoading = true;  
  331.   
  332.         //TODO  上拉加载对应操作  
  333.         // getMonitorAreasInfo('loadMore');  
  334.         //无网络环境请求数据,此处自行组装模拟数据  
  335.         // assembleAreasData("loadMore");  
  336.     }  
  337.   
  338.     /*****************************************item侧滑处理-start********************************************/  
  339.   
  340.     /* 
  341.      * item侧滑打开事件触发 
  342.      * 在item侧滑打开事件回调中获取当前item项数据 
  343.      * */  
  344.     $$(document).on('open''.equip_according_list_item'function () {  
  345.         swipeout_item_data = {};  
  346.         swipeout_item_data[0] = $$(this).attr("data-device_id");  
  347.         swipeout_item_data[1] = $$(this).attr("data-isConcerned");  
  348.         swipeout_item_data[2] = $$(this).attr("data-device_name");  
  349.         swipeout_item_data[3] = $$(this).attr("data-area_id");  
  350.     });  
  351.   
  352.     /* 
  353.      * 侧滑按钮点击事件 
  354.      * 在item侧滑按钮点击事件回调中处理数据变更 
  355.      * */  
  356.     $$(document).on('click''.equips_item_swipeout_btn'function () {  
  357.         //获取当前item项  
  358.         var item = page.find(".equip_according_list_item[data-device_id ='" + swipeout_item_data[0] + "']");  
  359.         if (swipeout_item_data[1] == 'true') {  
  360.             item.attr('data-isConcerned'false);  
  361.             // TODO 对应数据库操作和缓存操作  
  362.         } else {  
  363.             item.attr('data-isConcerned'true);  
  364.             // TODO 对应数据库操作和缓存操作  
  365.         }  
  366.         isOperated = true;  
  367.     });  
  368.   
  369.     /* 
  370.      * item侧滑关闭事件触发 
  371.      * 在item侧滑关闭事件回调中更新UI 
  372.      * 根据isOperated判断是否有点击侧滑按钮,处理对应逻辑 
  373.      * */  
  374.     $$(document).on('closed''.equip_according_list_item'function (e) {  
  375.         if (isOperated) {  
  376.             //有点击侧滑按钮  
  377.             if (swipeout_item_data[1] == 'true') {  
  378.                 //当前为关注状态时,点击后应更新UI为未关注状态  
  379.                 $$(this).find('.expanded_equips_item_state').attr('src''images/monitor/icon_concerns_no.png');  
  380.                 $$(this).find('.equips_item_swipeout_btn').css('background-color''darkorange');  
  381.                 $$(this).find('.equips_item_swipeout_btn').text('点击关注');  
  382.                 swipeout_item_data[1] = false;  
  383.             } else {  
  384.                 //当前为未关注状态时,点击后应更新UI为关注状态  
  385.                 $$(this).find('.expanded_equips_item_state').attr('src''images/monitor/icon_concerns_yes.png');  
  386.                 $$(this).find('.equips_item_swipeout_btn').css('background-color''lightgray');  
  387.                 $$(this).find('.equips_item_swipeout_btn').text('取消关注');  
  388.                 swipeout_item_data[1] = true;  
  389.             }  
  390.             isOperated = false;  
  391.         } else {  
  392.             //未点击侧滑按钮  
  393.   
  394.         }  
  395.     });  
  396.   
  397.     /*****************************************item侧滑处理-end********************************************/  
  398.   
  399.     return {  
  400.         init: init,  
  401.         beforeAnimal: beforeAnimal,  
  402.         resetState: resetState,  
  403.         refleshHandler: refleshHandler,  
  404.         loadMoreHandler: loadMoreHandler,  
  405.         bindEvent: bindEvent,  
  406.         destroyPage: destroyPage  
  407.     }  
  408. }();  

三,写在后面

1,本demo纯手撸,不足之处欢迎批评指正;

2,本demo是我从实际项目中剥离出来的,因本篇博客的主题是js二级列表的实现,所以已剔除网络请求、数据库操作、缓存操作、异常处理以及其他相关联部分实现代码;

3,在实际项目中,已使用此技术方案实现所有需求,但之后又因需求变更而被废弃;

4,后面产品经理要求一级列表和二级列表均能下拉刷新和上拉加载,还提出了一些其他的操作需求,一番蛋疼之后我使用全新技术方案都实现了,后面有空了再整理出来;

5,不明之处欢迎交流讨论;

猜你喜欢

转载自blog.csdn.net/xiaoxiaoluckylucky/article/details/79373835