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

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构建一个自己的组件

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,748评论 1 92
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,474评论 1 45
  • 一:什么是闭包?闭包的用处? (1)闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就 是将函数内部和函数外...
    xuguibin阅读 9,596评论 1 52
  • $HTML, HTTP,web综合问题 1、前端需要注意哪些SEO 2、 的title和alt有什么区别 3、HT...
    Hebborn_hb阅读 4,596评论 0 20
  • 时间盲注原理: 代码存在sql注入漏洞,然而页面既不会回显数据,也不会回显错误信息 语句执行后也不提示真假,我们不...
    shadowflow阅读 2,430评论 0 0