本篇文章讲述的是使用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,不明之处欢迎交流讨论;