components/app/app.vue

<template>
    <div class="ion-app" :version="version"
             :class="[modeClass,platformClass,hoverClass,{'disable-scroll':isScrollDisabled}]">
        <!--app-root start-->
        <div class="app-root">
            <slot></slot>
        </div>
        <!--modal portal-->
        <div id="modalPortal"></div>
        <!--蒙层指示,action-sheet,choose-sheet,picker等 sheetPortal-->
        <div id="sheetPortal"></div>
        <!--alert portal-->
        <div id="alertPortal"></div>
        <!--loading portal-->
        <div id="loadingPortal"></div>
        <!--toast portal-->
        <div id="toastPortal"></div>
        <!--当页面被点击的时候,防止在动画的过程中再次点击页面导致bug的蒙层,全局最高!z-index=99999-->
        <div class="click-block"
               :class="[{'click-block-enabled':isClickBlockEnabled}]"></div>
        <slot name="outer"></slot>
    </div>
</template>

<script type="text/javascript">
  /**
   * @component App
   * @description
   *
   *  ## 基础组件 / App组件
   *
   * App组件是Vimo框架的根组件,用于管理及控制整个页面的状态.
   * 控制App行为的方法已挂载到`vue.prototype.$app`上, 所以在页面中直接像这样使用, 例如:
   *
   * ```
   * this.$app.setTitle('Hello World'); // 设置title
   * this.$app.isScrolling; // 获取可滚动状态
   * this.$app.isEnabled; // 获取可点击状态
   * ```
   *
   * 此外, 弹出层挂载点也在此组件中列举, 可以用id搜索到这些挂载点:
   *
   * - modalPortal
   * - sheetPortal
   * - alertPortal
   * - loadingPortal
   * - toastPortal
   *
   * ### 何为基础组件
   *
   * 因为业务的复杂多样, 如果组件全部加载, 会造成初始化的下载包过大, 所以基础组件(SPA应用框架组件)在安装Vimo的时候就全局安装, 不需要在业务中再次安装. 除此之外的组件则要按需引入.
   *
   * 布局组件就是基础组件, 比如: **App/Nav/Navbar/Page/Header/Footer/Content**, 一共7个. 这些组件不是业务组件, 组件由vimo初始化安装完毕. 此外, 如果不使用布局组件也没关系. 通过布局组件提供的方法就能知道它的作用了.
   *
   * - App: 根组件, 用于设置页面点击/滚动状态/设置文档title/设置根位置的样式
   * - Nav: 导航组件, 用于设置页面切换属性: 转场动画/转场是否显示Indicator/和Menus配合
   * - Page: 页面容器, 因为Vue要求业务template必须要使用一个根元素包裹, 故Page就是这个作用. 此外, 通过页面切换定义z-index层级.
   * - Header: 顾名思义, 页面的头部, 这部分不会页面滚动, 内部可安插Toolbar/Navbar等组件
   * - Footer: 使用方法和Header一致
   * - Content: 页面展示部分的容器, 可以使滚动内容, 也可以是固定内容, 详情参考组件介绍
   * - Nabvar: 该组件也在基础组件中, Navbar存在的意义是定义内容Title组件, 用于更新文档的Title. 页面切换在左侧显示后退按钮, 也可定义多个按钮. 使用方法和 Toolbar 一致.
   *
   * ### 可用状态(参考示例)
   *
   * - isScrolling  获取当前可滚动状态
   * - isEnabled    获取可点击状态
   *
   * ### 可在全局使用的公共样式
   *
   * -Text Alignment
   *    - [text-left]          - 文本左对齐
   *    - [text-center]        - 文本居中
   *    - [text-right]         - 文本右对齐
   *    - [text-justify]       - 文本右对齐
   *    - [text-nowrap]        - 文本不换行
   *
   *
   * -Text Transformation
   *    - [text-uppercase]     - 文本大写
   *    - [text-lowercase]     - 文本小写
   *    - [text-capitalize]    - 文本首字母大写
   *
   * -Normal
   *    - [padding]         - 结构增加padding, 默认16px
   *    - [no-padding]      - 结构去除padding
   *    - [hidden]          - display:none
   *    - .hidden           - display:none
   *
   * @props {String} [mode='ios'] - 模式, 用于在根处定义app的平台及样式
   *
   *
   * @slot 空               默认插入到正常页面中
   * @slot [outer]         插入到最外部, 用于定义在所有页面和弹出层之上的结构组件,比如:landscape-prompt组件
   *
   * @demo #/app
   **/

  import { isString, isPresent, setElementClass } from '../../util/util'
  import ThemeMixins from '../../themes/theme.mixins'
  import ClickBlock from './click-block'

  const CLICK_BLOCK_BUFFER_IN_MILLIS = 64       // click_blcok等待时间
  const CLICK_BLOCK_DURATION_IN_MILLIS = 700    // 时间过后回复可点击状态
  const clickBlockInstance = new ClickBlock()

  let scrollDisTimer = null                     // 计时器
  export default {
    name: 'vm-app',
    mixins: [ThemeMixins],
    data () {
      return {
        disabledTimeRecord: 0,          // 禁用计时
        scrollTimeRecord: 0,            // 滚动计时
        isScrollDisabled: false,        // 控制页面是否能滚动
        isClickBlockEnabled: false,     // 控制是否激活 '冷冻'效果 click-block-enabled

        isScrolling: false,             // 可滚动状态
        isEnabled: true,                // 可点击状态

        version: isPresent(window.VM) && window.VM.version
      }
    },
    computed: {
      modeClass () {
        return `${this.mode}`
      },
      platformClass () {
        return `platform-${this.mode}`
      },
      hoverClass () {
        let _isMobile = navigator.userAgent.match(/AppleWebKit.*Mobile.*/)
        return _isMobile ? 'disable-hover' : 'enable-hover'
      }
    },
    methods: {
      /**
       * @function setEnabled
       * @description
       * 设置当前页面是否能点击滑动, 一般使用在像ActionSheet/Alert/Modal等弹出会出现transition动画,
       * 当transition动画进行中,页面是锁定的不能点击,因此使用该函数设定App的状态, 保证动画过程中, 用户不会操作页面
       * @param {boolean} isEnabled  - `true` for enabled, `false` for disabled
       * @param {number} duration - isEnabled=false的过期时间 当 `isEnabled` 设置为`false`, 则duration之后,`isEnabled`将自动设为`true`
       *
       * @example
       * this.$app && this.$app.setEnabled(false, 400) -> 400ms内页面不可点击, 400ms过后可正常使用
       * this.$app && this.$app.setEnabled(true) -> 64ms后解除锁定
       **/
      setEnabled (isEnabled, duration = CLICK_BLOCK_DURATION_IN_MILLIS) {
        this.disabledTimeRecord = (isEnabled ? 0 : Date.now() + duration)
        this.isEnabled = isEnabled
        if (isEnabled) {
          // disable the click block if it's enabled, or the duration is tiny
          clickBlockInstance.activate(false, CLICK_BLOCK_BUFFER_IN_MILLIS).then(() => {
            this.isEnabled = true
          })
        } else {
          // show the click block for duration + some number
          clickBlockInstance.activate(true, duration + CLICK_BLOCK_BUFFER_IN_MILLIS).then(() => {
            this.isEnabled = true
          })
        }
      },

      /**
       * @function setDisableScroll
       * @description
       * 是否点击滚动, 这个需要自己设置时间解锁
       * @param {Boolean} isScrollDisabled - 是否禁止滚动点击 true:禁止滚动/false:可以滚动
       * @param {number} duration - 时间过后则自动解锁
       * @example
       * this.$app && this.$app.setDisableScroll(true, 400) -> 400ms内页面不可滚动, 400ms过后可正常使用
       * this.$app && this.$app.setDisableScroll(false) ->立即解除锁定
       **/
      setDisableScroll (isScrollDisabled, duration = 0) {
        if (duration > 0 && isScrollDisabled) {
          this.isScrollDisabled = isScrollDisabled
          window.clearTimeout(scrollDisTimer)
          scrollDisTimer = window.setTimeout(() => {
            this.isScrollDisabled = false
          }, duration)
        }
      },

      /**
       * @function setClass
       * @description
       * 设置根组件的class样式, 比如全局颜色替换或者结构变更
       * @param {string} className - 样式名称
       * @param {boolean} [isAdd=false] - 是否添加
       */
      setClass (className, isAdd = false) {
        if (className) {
          setElementClass(this.$el, className, isAdd)
        }
      },

      /**
       * @function setDocTitle
       * @param {String|Object}  _title - 设置标题
       * @param {String}  _title.title - 标题
       * @description
       * 设置document.title的值, 如果传入的是string, 则为title的字符串, 如果是对象, 则title字段为标题名称
       **/
      setDocTitle (_title) {
        if (isString(_title)) {
          _title = {title: _title}
        }
        // BugFixed: 如果组件不是通过异步加载, 则他的执行顺序会很靠前, 此时平台的方法并未初始化完毕. 因此异步定时后在执行
        window.setTimeout(() => {
          let isHandled = !!this.$platform && !!this.$platform.setNavbarTitle && this.$platform.setNavbarTitle(_title)
          if (!isHandled) {
            if (this.$platform && this.$platform.platforms().length <= 2) {
              // PC端
              document.title = _title.title || ''
            } else {
              // 利用iframe的onload事件刷新页面
              document.title = _title.title
              let iframe = document.createElement('iframe')
              // 空白图片
              iframe.src = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=='
              iframe.style.visibility = 'hidden'
              iframe.style.width = '1px'
              iframe.style.height = '1px'
              iframe.onload = function () {
                window.setTimeout(function () {
                  document.body.removeChild(iframe)
                }, 0)
              }
              document.body.appendChild(iframe)
            }
          }
        }, 16 * 5)
      }
    },
    created () {
      console.assert(this.$platform, `The Component of <vm-app> need 'platform' instance`)
      console.assert(this.$config, `The Component of <vm-app> need 'config' instance`)

      /**
       * $app对外方法
       **/
      let proto = Reflect.getPrototypeOf(Reflect.getPrototypeOf(this))
      proto.$app = this

      const _this = this
      this.$eventBus.$on('onScrollStart', () => {
        _this.isScrolling = true
      })
      this.$eventBus.$on('onScroll', () => {
        _this.isScrolling = true
      })
      this.$eventBus.$on('onScrollEnd', () => {
        _this.isScrolling = false
      })

      // 设置当前可点击
      this.isClickBlockEnabled = true
    },
    mounted () {
      if (window.VM) {
        window.VM.$app = this
        // 用于判断组件是否在VM的组件树中
        window.VM.$root = this.$root
      }
    }
  }
</script>

<style lang="scss">
    @import 'app';
    @import 'app.ios';
    @import 'app.md';

    // Page Animate
    // --------------------------------------------------
    @import "transition/fade-bottom-transition";
    @import "transition/fade-right-transition";
    @import "transition/fade-transition";
    @import "transition/ios-transition";
    @import "transition/zoom-transition";
</style>