深入解读JavaScript让前端飞JavaScript 进阶营

小程序实现在线选座实战(上)

2019-03-17  本文已影响1人  悟C

hi~ 大家好,我叫内孤,一名web前端开发者/:B-,今天我来分享一个移动端在线选座的功能页面,我们知道微信小程序可以用web-view嵌入html页面,所以这个选座功能我们就用html、css、javascript一步一步实现。避免篇幅太长,我将整个功能的实现分为上、中、下。

假设下图就是我们要实现的功能页面,我们先对这个功能页面进行组件划分,header、main、footer三个组件来分别实现:


image.png

1. 创建index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1.0,user-scalable=no,viewport-fit=cover"/>
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <!-- 初始化样式 -->
  <link rel="stylesheet" href="./css/reset.css">
  <link rel="stylesheet" href="./css/styles.css">
  <!-- rem 的解决方案 -->
  <script type="text/javascript">
  (function(e,t){var i=document,n=window;var l=i.documentElement;var r,a;var d,o=document.createElement("style");var s;function m(){var i=l.getBoundingClientRect().width;if(!t){t=540}if(i>t){i=t}var n=i*100/e;o.innerHTML="html{font-size:"+n+"px;}"}r=i.querySelector('meta[name="viewport"]');a="width=device-width,initial-scale=1,maximum-scale=1.0,user-scalable=no,viewport-fit=cover";if(r){r.setAttribute("content",a)}else{r=i.createElement("meta");r.setAttribute("name","viewport");r.setAttribute("content",a);if(l.firstElementChild){l.firstElementChild.appendChild(r)}else{var c=i.createElement("div");c.appendChild(r);i.write(c.innerHTML);c=null}}m();if(l.firstElementChild){l.firstElementChild.appendChild(o)}else{var c=i.createElement("div");c.appendChild(o);i.write(c.innerHTML);c=null}n.addEventListener("resize",function(){clearTimeout(s);s=setTimeout(m,300)},false);n.addEventListener("pageshow",function(e){if(e.persisted){clearTimeout(s);s=setTimeout(m,300)}},false);if(i.readyState==="complete"){i.body.style.fontSize="16px"}else{i.addEventListener("DOMContentLoaded",function(e){i.body.style.fontSize="16px"},false)}})(750,750);
  </script>
  <title>在线选座</title>
</head>
<body>
  <div class="page">

    <div class="header">
    </div>

    <div class="main">

    </div>

    <div class="footer">

    </div>
    
  </div>
</body>
</html>

这里我们定义了header、main、footer容器,分别来装载三个组件,所有的样式都放在styles.css中,reset.css是来重置样式,结合header里的那段脚本实现rem不同屏幕自适应。

2. 书写对应的布局样式(styles.css)

/* 基本布局样式 */
.page {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  color: #999;
}
.header {
  height: .9rem;
  overflow: hidden;
  background-color: #fff;
}
.main {
  background-color: #eee;
  flex-grow: 1;
  overflow: hidden;
}
.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 1.2rem;
  background-color: #fff;
}
.mrgR60 {
  margin-right: 0.6rem;
}

3. reset.css

  body,dl,dd,ul,ol,h1,h2,h3,h4,h5,h6,pre,form,input,textarea,p,hr,thead,tbody,tfoot,th,td{margin:0;padding:0;}
  ul,ol{list-style:none;}
  a{text-decoration:none;}
  html{-ms-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none;font-size:50px;}
  body{line-height:1.5;font-size:16px;}
  body,button,input,select,textarea{font-family:'helvetica neue',tahoma,'hiragino sans gb',stheiti,'wenquanyi micro hei',\5FAE\8F6F\96C5\9ED1,\5B8B\4F53,sans-serif;}
  b,strong{font-weight:bold;}
  i,em{font-style:normal;}
  table{border-collapse:collapse;border-spacing:0;}
  table th,table td{border:1px solid #ddd;padding:5px;}
  table th{font-weight:inherit;border-bottom-width:2px;border-bottom-color:#ccc;}
  img{border:0 none;width:auto\9;max-width:100%;vertical-align:top;}
  button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;vertical-align:baseline;}
  button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}
  button[disabled],input[disabled]{cursor:default;}
  input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}
  input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}
  input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}
  @media screen and (-webkit-min-device-pixel-ratio:0){
  input{line-height:normal!important;}
  }
  select[size],select[multiple],select[size][multiple]{border:1px solid #AAA;padding:0;}
  article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}
  audio,canvas,video,progress{display:inline-block;}


  .g-doc{width:7.5rem;margin:0px auto;}

4. 组件1: 座位状态示意图

这里没有互动,是很简单的展示组件

// 在index.html中的header中添加

<div class="header">
  <div class="seatStatusList">
    <div class="statusLabel mrgR60">
      <img src="./image/seat.png"/>
      <span>已选中</span>
    </div>
    <div class="statusLabel">
      <img src="./image/seat_disabled.png"/>
      <span>不可选</span>
    </div>
  </div>
</div>

// 添加到styles.css中,座位状态组件
.seatStatusList {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 1rem;
}
.seatStatusList .statusLabel img {
  width: .5rem;
  height: .4rem;
  margin-right: .1rem;
}

到这一步我们可以看到基本的架子了

image.png

5. 实现底部的组件

这个组件我们就创建一个footer.js实现

var Footer = (function(factory) {
  return factory.call();
}(function() {
  // 定义默认的回调
  var __DESC__ = {
    onClickInfoModule: function() {},
    onHandleSure: function() {},
    /**
     * 默认格式化数据的回调
     * @param {Array} data 例如:[{id: 1, price: 2}]
     * 注意: 如果没有定义formatData回调,需要确保item中包含price
     * @return {total, count}
     */
    formatData: function(data) {
     var total = 0, count = 0, res = {};
     if (Object.prototype.toString.call(data) === "[object Array]") {
      for (var i = 0, len = data.length; i < len; i++) {
        count++;
        if (data[i].price) {
          total += data[i].price;
        } else {
          new Error('座位信息中没有price字段');
          break;
        }
      }
      res.total = total;
      res.count = count;
     } else {
      new Error('data 不是一个数组');
     }
     return res;
    }
  };
  var __CORE__ = {
    init: function(options) {
      this.$el = document.querySelector(options.el);
      this.onHandleSure = options.onHandleSure || __DESC__.onHandleSure;
      this.formatData = options.formatData || __DESC__.formatData;
      this.onClickInfoModule = options.onClickInfoModule || __DESC__.onClickInfoModule;
      this.data = [];
      this._renderDefaultTpl();
      this._onClickSureBtn();
      this._onClickInfoModule();
      return this;
    },
    // 监听点击了信息模块的回调
    _onClickInfoModule: function () {
      var me = this;

      me.$el.addEventListener('touchstart', function(e) {
        var target = e.target;
        var parentNode = target.parentNode;
        if (parentNode.className && parentNode.className.indexOf('priceBox') > -1 || parentNode.parentNode.className && parentNode.parentNode.className.indexOf('priceBox') > -1) {
          if (typeof me.onClickInfoModule === 'function') {
            me.onClickInfoModule.call(me, me.data);
          }
        }
      });
    },
    // 监听确定选座按钮
    _onClickSureBtn: function() {
      var me = this;
      me.$el.addEventListener('touchstart', function(e) {
        var target = e.target;
        // 用me.$el 代理点击事件
        if (target.className && target.className.indexOf('sureBtn') > -1) {
          if (typeof me.onHandleSure === 'function') {
            me.onHandleSure.call(me, me.data);
          }
        }
      });
    },
    // 私有方法:渲染选中的座位信息
    _renderSelectedSeatInfo: function(total, count) {
      var tpl = '<div class="priceBox"><i>应付: <span class="price">' + total + '元' + '</span></i>' +
        '<span>共' + count + '张</span></div>';
      this.$el.querySelector('.total').innerHTML = tpl;
    },
    // 私有方法:渲染默认的组件状态
    _renderDefaultTpl: function() {
      var tpl = ('<div class="footer-component">' +
        '<div class="total">' +
        '请选择座位' +
        '</div>' +
        '<div class="sureBtn">确定选座</div>' +
        '</div>');
      this.$el.innerHTML = tpl;
    },
    /**
     * 
     * @param {*} data 传入的数据
     * 如果没有自定义formatData回调,约定data数据中必须包含price, 例如: [{ price: 2 }]
     */
    setData: function(data) {
      var res = {};
      this.data = data;
      if (typeof this.formatData === 'function') {
        res = this.formatData(data);
      }
      if (res && res.total && res.count) {
        this._renderSelectedSeatInfo(res.total, res.count);
      } else {
        new Error('formatData 返回的参数没有total和count');
      }
    },
    // 重置初始化状态
    resetStatus: function() {
      this.data = [];
      this._renderDefaultTpl();
    }
  };

  return __CORE__;
}));

以上我们用闭包创建了一个Footer组件,通过Footer.init实现组件初始化,对外留着一个setData方法,用来设置约定格式的数据然后进行视图渲染。还有一个resetStatus方法,来重置状态和视图。

然后我们在index.js对组件进行初始化

// 监听document加载完毕才去初始化各个组件
document.addEventListener("DOMContentLoaded", function (e) {
  Footer.init({
    el: '.footer',
    onHandleSure: function(data) {
      console.log('点击确定等到我们选中的座位信息,发送给服务器', data);
    },
    onClickInfoModule: function(data) {
      console.log('点击了信息模块', data);
    }
  });
  var selectedData = [
    {
      id: 1,
      price: 1,
    },
    {
      id: 2,
      price: 2
    }
  ];

  Footer.setData(selectedData);

  setTimeout(function() {
    Footer.resetStatus();
  }, 2000);
});

创建了Footer组件后,我们完成的界面如下:


image.png

回顾

  1. 在整体布局的搭建中我们使用了rem自适应屏幕大小方案
  2. 在Footer组件中,我们使用了闭包构建组件、使用了事件代理等,实现了如何用javascript构建一个自己的组件

接下去,我们将在《小程序实现在线选座实战(中)》实现选座组件,在《小程序实现在线选座实战(下)》中实现数据交互。

上一篇下一篇

猜你喜欢

热点阅读