本篇文章讲述的是使用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
<div class="pages navbar-through"> <div data-page="monitor_main" class="page"> <div class="page-header" style="position: absolute;z-index:9"> <div class="page-header-inner"> <div class="left"> <a href="#" class="link"> <img src="images/monitor/menu.png" class="nav_open_panel"> </a> </div> <div class="center">JS二级列表</div> <div class="right"> <a href="#"></a> </div> </div> </div> <div style="width:100%; margin-top:0px; height:100%;" id="monitor_areas_list" class="page-content pull-to-refresh-content infinite-scroll" data-ptr-distance="55" data-distance="1"> <div class="pull-to-refresh-layer"> <div class="preloader cus-preloader"></div> <div class="pull-to-refresh-arrow"></div> </div> <div class="list-block cusv1-accordion-list" style="margin: 48px 0"> <ul id="areas_ul"> </ul> </div> </div> </div> </div> 2,列表使用模板 monitor_main_tpl.html <li class="cusv1-accordion-item area_accordion_item" data-area_id={{item_area_id}} data-area_index={{item_area_index}}> <a href="#" class="item-link item-content accordion-item-header" style="display:block"> <div class="area_list_item_unit1"> <span class="area_list_source">{{item_area_name}}</span> <span class="area_equip_num">{{item_equip_num}}</span> <img src="images/monitor/icon_arrow_to_right.png" class="icon_to_expand_area"> </div> <div class="area_list_item_unit2"> <img src="images/monitor/monitor_location_icon.png" class="loc_icon"/> <span class="equip_list_location" style="width:80%">{{item_area_location}}</span> </div> </a> <div class="accordion-item-content"> <div class="list-block"> <ul style="padding-left: 0"> {{#each subs}} <li class="swipeout equip_according_list_item" data-area_id={{area_id}} data-device_id={{device_id}} data-device_name={{device_name}} data-isConcerned={{isConcerned}}> <div class="swipeout-content"> <div class="concerns_click_area"> <img class="expanded_equips_item_state" src="{{concerns_state_icon}}"/> </div> <div class="expanded_equips_click_area"> <span class="expanded_equip_item_source">{{device_name}}</span> <img src="images/monitor/icon_arrow_to_right.png" class="icon_to_equip_details"> </div> </div> <div class="swipeout-actions-right"> <a href="#" class="swipeout-close cancle_concerns equips_item_swipeout_btn" style="background-color:{{btn_item_left_bg}}">{{btn_item_left_text}}</a> </div> </li> {{/each}} </ul> </div> </div> </li> 3,收缩列表工具类 accordion.js
/*=============================================================================== ************ Accordion ************ ===============================================================================*/ (function () { var $ = $$, app = myApp; app.accordionOpenv1 = function (item) { item = $(item); var list = item.parents('.cusv1-accordion-list').eq(0); var content = item.children('.accordion-item-content'); if (content.length === 0) content = item.find('.accordion-item-content'); var expandedItem = list.length > 0 && item.parent().children('.accordion-item-expanded'); if (expandedItem.length > 0) { app.accordionClose(expandedItem); } content.css('height', content[0].scrollHeight + 'px').transitionEnd(function () { if (item.hasClass('accordion-item-expanded')) { content.transition(0); content.css('height', 'auto'); var clientLeft = content[0].clientLeft; content.transition(''); item.trigger('opened'); } /* else { content.css('height', ''); item.trigger('closed'); } */ }); item.trigger('open'); item.addClass('accordion-item-expanded'); }; app.accordionClosev1 = function (item) { item = $(item); var content = item.children('.accordion-item-content'); if (content.length === 0) content = item.find('.accordion-item-content'); item.removeClass('accordion-item-expanded'); content.transition(0); content.css('height', content[0].scrollHeight + 'px'); // Relayout var clientLeft = content[0].clientLeft; // Close content.transition(''); content.css('height', '').transitionEnd(function () { /* if (item.hasClass('accordion-item-expanded')) { content.transition(0); content.css('height', 'auto'); var clientLeft = content[0].clientLeft; content.transition(''); item.trigger('opened'); } else {*/ content.css('height', ''); item.trigger('closed'); // } }); item.trigger('close'); }; })(); 4,逻辑实现 monitor_main.js
/** * Created on 2016/12/18. */ var $$ = Dom7; var myApp = new Framework7(); var mainView = myApp.addView('.view-main'); var utils = { post: function (data, sf, ef, p) { var url = utils.postUrl(); $$.ajax({ url: url, async: true, method: 'POST', contentType: 'text/plain', crossDomain: true, data: data, success: function (e) { if ("function" == typeof sf) sf(e, p); }, error: function (e) { console.log(e); if ("function" == typeof ef) ef(e, p); } }); }, get: function (url, sf, ef, p) { $$.ajax({ url: url, async: true, method: 'GET', contentType: 'application/x-www-form-urlencoded', crossDomain: true, success: function (e) { if ('function' == typeof sf) sf(e, p); }, error: function (e) { if ('function' == typeof ef) ef(e, p); } }); } } var monitorMain = function () { var page, refreshContent, infiniteScroll, isLoading = false, isAreasLoading = false, data , templateStr, template, distreeData, cacheDatas = {}, isOperated = false, swipeout_item_data = {}; /* * 获取模板文本 * */ $$.get("pages/monitor/monitor_main_tpl.html", function (data) { templateStr = data; }); /* * 加载监控页面 * */ mainView.router.load( { url: 'pages/monitor/monitor_main.html' } ); /* * 页面pageBeforeAnimation回调 * */ $$(document).on('pageBeforeAnimation', function (e) { var page = e.detail.page; if (page.name == "monitor_main") { beforeAnimal(page); } }); /* * 页面pageBeforeAnimation回调执行动作 * */ function beforeAnimal(page) { var container = $$(page.container) container.find('.navbar').removeClass('navbar-hidden'); container.find('.pull-to-refresh-content').css({'margin-top': '-44px', 'padding-bottom': '44px'}); container.find('.pull-to-refresh-layer').css({'margin-top': '0'}); } /* * 页面pageInit回调 * */ $$(document).on('pageInit', function (e) { page = e.detail.page; if (page.name == "monitor_main") { init(page); } }); /* * 页面pageInit回调执行动作 * */ function init(p) { page = $$(p.container); refreshContent = page.find('.pull-to-refresh-content'); infiniteScroll = page.find('.infinite-scroll'); template = Template7.compile(templateStr); //查询数据 beginQuery(); //绑定事件 bindEvent("on"); //构建文本 buildAreasBox(distreeData); // 缓存数据-保存数据到sessionStorage,缓存模块相关操作逻辑代码较多,此demo中已去除 // sessionStorage.setItem("distreeData", JSON.stringify(distreeData)); } /* * 查询数据 * 先从缓存获取数据,缓存数据为空时再网络请求/模拟数据 * 缓存模块相关操作逻辑代码较多,因缓存相关非此demo的重点,此demo中已去除 * */ function beginQuery() { // distreeData = JSON.parse(sessionStorage.getItem("distreeData")); if (distreeData == null) { // getMonitorAreasInfo("refresh"); //无网络环境请求数据,此处自行组装模拟数据 assembleAreasData("refresh"); } else { buildAreasBox(distreeData); } } /* * 绑定/卸载事件 * */ function bindEvent(t) { var method = t == "on" ? t : "off"; if (refreshContent) { //列表下拉刷新事件 refreshContent[method]('refresh', refleshHandler); } if (infiniteScroll) { //列表上拉加载更多事件 infiniteScroll[method]('infinite', loadMoreHandler); } if (page) { //二级列表打开/收缩事件 page[method]('click', ".accordion-item-header", accordionToggle); } } /* * 构建列表文本 * */ function buildAreasBox(items) { data = items; var i, item, htmlstr = '', areaUl = page.find("#areas_ul"); for (i = 0; i < items.length; i++) { item = items[i]; htmlstr += template(item).trim(); } //给列表插入文本 areaUl.html(htmlstr); } /* * 渲染 * 非此demo的重点,此demo中已去除相关代码 * */ function render(items) { cacheDatas = items; for (var key in items) { var el = page.find(".data-" + key); el.text(items[key]); } } /* * 查询数据 * */ function queryData() { if (isLoading) { resetState(); return; } isLoading = true; // getMonitorAreasInfo("refresh"); //无网络环境请求数据,此处自行组装模拟数据 assembleAreasData("refresh"); } /* * 重置状态 * */ function resetState() { isLoading = false; myApp.pullToRefreshDone(); myApp.hideIndicator(); } /* * destroy页面 * */ function destroyPage(p) { console.log("destroy warning main page"); cacheDatas = {}; bindEvent("off"); } /* * 模拟一级列表(区域)数据 * */ function assembleAreasData(type) { distreeData = []; for (var i = 0; i < 12; i++) { var areaId = '0_' + i; var subsData = simulateEquipDatas(areaId); distreeData[i] = { item_area_index: i, item_area_id: areaId, item_area_name: '数控机床区' + i, item_equip_num: subsData.length, item_area_location: '华南地区/深圳分公司/' + i + '号厂房/中区', subs: subsData } } } /* * 模拟二级列表(设备)数据 * */ function simulateEquipDatas(areaId) { var equipsArr = []; for (var i = 0; i < 6; i++) { equipsArr[i] = { concerns_state_icon: "images/monitor/icon_concerns_no.png", isConcerned: false, btn_item_left_bg: 'darkorange', btn_item_left_text: '点击关注', device_id: '0_1_' + i, device_name: '数控切割机' + i, area_id: areaId } } return equipsArr; } /* * 网络请求一级列表(区域)数据 * */ function getMonitorAreasInfo(type) { var data = JSON.stringify('post_data'); var params = { url: 'http://***', data: data } utils.post(params, function (e) { resetState(); distreeData = []; // distreeData = }, function (e) { console.log("连接服务器失败,请检查网络"); resetState(); }); } /* * 网络请求二级列表(设备)数据 * */ function getEquipsInfoByAreaId(area_id, _callback) { var data = {} data = JSON.stringify(data); var params = { url: 'http://***', data: data } utils.post(params, function (e) { var equipsArr = []; _callback(equipsArr); }, function (e) { console.log("连接服务器失败,请检查网络"); resetState(); }); } /* * 二级列表打开/收缩事件 * */ function accordionToggle(e) { var item = $$(e.target).parents(".cusv1-accordion-item"); if (item.length === 0) return; if (item.hasClass('accordion-item-expanded')) { item.find(".icon_to_expand_area").attr("src", "images/monitor/icon_arrow_to_right.png"); myApp.accordionClosev1(item); } else { myApp.accordionOpenv1(item); item.find(".icon_to_expand_area").attr("src", "images/monitor/icon_arrow_to_bottom.png"); } } /* * 刷新事件-列表下拉刷新 * */ function refleshHandler(e) { console.log("区域列表下拉刷新"); myApp.showIndicator(); queryData(); myApp.hideIndicator(); // 加载完毕需要重置 myApp.pullToRefreshDone(); } /* * 加载事件-列表上拉加载更多 * */ function loadMoreHandler(e) { console.log("区域列表上拉加载更多"); // 如果正在加载,则退出 if (isAreasLoading) return; // 设置flag isAreasLoading = true; //TODO 上拉加载对应操作 // getMonitorAreasInfo('loadMore'); //无网络环境请求数据,此处自行组装模拟数据 // assembleAreasData("loadMore"); } /*****************************************item侧滑处理-start********************************************/ /* * item侧滑打开事件触发 * 在item侧滑打开事件回调中获取当前item项数据 * */ $$(document).on('open', '.equip_according_list_item', function () { swipeout_item_data = {}; swipeout_item_data[0] = $$(this).attr("data-device_id"); swipeout_item_data[1] = $$(this).attr("data-isConcerned"); swipeout_item_data[2] = $$(this).attr("data-device_name"); swipeout_item_data[3] = $$(this).attr("data-area_id"); }); /* * 侧滑按钮点击事件 * 在item侧滑按钮点击事件回调中处理数据变更 * */ $$(document).on('click', '.equips_item_swipeout_btn', function () { //获取当前item项 var item = page.find(".equip_according_list_item[data-device_id ='" + swipeout_item_data[0] + "']"); if (swipeout_item_data[1] == 'true') { item.attr('data-isConcerned', false); // TODO 对应数据库操作和缓存操作 } else { item.attr('data-isConcerned', true); // TODO 对应数据库操作和缓存操作 } isOperated = true; }); /* * item侧滑关闭事件触发 * 在item侧滑关闭事件回调中更新UI * 根据isOperated判断是否有点击侧滑按钮,处理对应逻辑 * */ $$(document).on('closed', '.equip_according_list_item', function (e) { if (isOperated) { //有点击侧滑按钮 if (swipeout_item_data[1] == 'true') { //当前为关注状态时,点击后应更新UI为未关注状态 $$(this).find('.expanded_equips_item_state').attr('src', 'images/monitor/icon_concerns_no.png'); $$(this).find('.equips_item_swipeout_btn').css('background-color', 'darkorange'); $$(this).find('.equips_item_swipeout_btn').text('点击关注'); swipeout_item_data[1] = false; } else { //当前为未关注状态时,点击后应更新UI为关注状态 $$(this).find('.expanded_equips_item_state').attr('src', 'images/monitor/icon_concerns_yes.png'); $$(this).find('.equips_item_swipeout_btn').css('background-color', 'lightgray'); $$(this).find('.equips_item_swipeout_btn').text('取消关注'); swipeout_item_data[1] = true; } isOperated = false; } else { //未点击侧滑按钮 } }); /*****************************************item侧滑处理-end********************************************/ return { init: init, beforeAnimal: beforeAnimal, resetState: resetState, refleshHandler: refleshHandler, loadMoreHandler: loadMoreHandler, bindEvent: bindEvent, destroyPage: destroyPage } }();
1,本demo纯手撸,不足之处欢迎批评指正;
2,本demo是我从实际项目中剥离出来的,因本篇博客的主题是js二级列表的实现,所以已剔除网络请求、数据库操作、缓存操作、异常处理以及其他相关联部分实现代码;
3,在实际项目中,已使用此技术方案实现所有需求,但之后又因需求变更而被废弃;
4,后面产品经理要求一级列表和二级列表均能下拉刷新和上拉加载,还提出了一些其他的操作需求,一番蛋疼之后我使用全新技术方案都实现了,后面有空了再整理出来;
5,不明之处欢迎交流讨论;
