app/app.vue

<template>
    <article class="ion-app" :version="version" :id="mode"
             :style="styleObj"
             :class="[mode,modeClass,platformClass,{'disable-hover':disableHover},{'disable-scroll':disableScroll}]">
        <section class="app-root">
            <slot></slot>
        </section>
        <aside id="modalPortal"></aside>
        <aside id="sheetPortal"></aside>
        <aside id="alertPortal"></aside>
        <aside id="loadingPortal"></aside>
        <aside id="toastPortal"></aside>
        <aside class="click-block"
               :class="[{'click-block-enabled':isClickBlockEnabled,'click-block-active':isClickBlockActive}]"></aside>
        <slot name="outer"></slot>
    </article>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
  import { setElementClass } from '../../util/util'
  import { isPresent, isString } from '../../util/type'
  import disableHover from '../../util/disable-hover'
  import modeMixins from '../../util/mode-mixins.js'

  const CLICK_BLOCK_BUFFER_IN_MILLIS = 64       // 等待业务完毕的额外时间
  const CLICK_BLOCK_DURATION_IN_MILLIS = 700    // 时间过后回复可点击状态

  export default {
    name: 'App',
    mixins: [modeMixins],
    provide () {
      let _this = this
      return {
        appComponent: _this
      }
    },
    data () {
      return {
        disableHover: disableHover,     // 禁用计时
        isScrollDisabled: false,        // 控制页面是否能滚动
        disableScroll: false,

        isClickBlockEnabled: false,     // 控制是否能点击 click-block-enabled
        isClickBlockActive: false,      // 控制是否激活 '冷冻'效果 click-block-active
        clickBlockTimer: null,
        disableScrollTimer: null,

        isScrolling: false,             // 可滚动状态
        isEnabled: true,                // 可点击状态
        scrollTop: 0,                   // 记录scrollTop, 用于disable scroll
        styleObj: {},

        version: isPresent(window.VM) && window.VM.version
      }
    },
    computed: {
      platformClass () {
        return `platform-${this.mode}`
      }
    },
    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) {
        if (isEnabled) {
          // 解锁
          if (this.isClickBlockActive) {
            this.timer && window.clearTimeout(this.timer)
            this.timer = window.setTimeout(() => {
              this.isClickBlockActive = false
              this.isEnabled = true
            }, CLICK_BLOCK_BUFFER_IN_MILLIS)
          } else {
            this.isEnabled = true
          }
        } else {
          // 枷锁
          this.isEnabled = false
          this.isClickBlockActive = true
          this.timer && window.clearTimeout(this.timer)
          this.timer = window.setTimeout(() => {
            this.isClickBlockActive = false
            this.isEnabled = true
          }, duration + CLICK_BLOCK_BUFFER_IN_MILLIS)
        }
      },

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

      /**
       * @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) {
            document.title = _title.title || ''
          }
        }, 16 * 5)
      },

      /**
       * @param {Boolean} shouldShow
       * @param {Number} [expire=100]
       * @return {Promise}
       * @private
       * */
      $_activate (shouldShow, expire = 100) {
        return new Promise((resolve) => {
          if (this.isEnabled !== shouldShow) {
            this.isEnabled = shouldShow

            window.setTimeout(() => {
              this.isEnabled = false
            }, expire)
          } else {
            resolve()
          }
        })
      },

      /**
       * @private
       * */
      $_disableScroll () {
        if (!this.isScrollDisabled) {
          this.$root.$emit('app:disableScroll')
          this.scrollTop = 0
          this.styleObj = {}
          this.isScrollDisabled = true

          // 如果页面高度小于窗口高度, 则不进行限制(限制没意义)
          let windowHeight = window.innerHeight
          let docHeight = window.document.documentElement.offsetHeight
          if (windowHeight >= docHeight) {
            this.disableScroll = false
            // `窗口高度:${windowHeight}, 页面高度: ${docHeight}, 当前情况setDisableScroll不生效.`
          } else {
            this.disableScroll = true
            this.scrollTop = window.scrollY
            this.styleObj = {
              top: -this.scrollTop + 'px'
            }
          }
        }
      },

      /**
       * @private
       * */
      $_enableScroll () {
        if (this.isScrollDisabled) {
          this.$root.$emit('app:enableScroll')
          this.isScrollDisabled = false
          this.disableScroll = false
          this.$nextTick(() => {
            // scrollTop lost after set position:fixed, restore it back.
            let windowHeight = window.innerHeight
            let docHeight = window.document.documentElement.offsetHeight
            if (windowHeight < docHeight && this.scrollTop > 0) {
              window.scrollTo(0, this.scrollTop)
            }
          })
        }
      }
    },
    created () {
      // eslint-disable-next-line no-proto
      let proto = this.__proto__.__proto__
      proto.$app = this
      proto.$root = this.$root
      this.$root.$on('onScrollStart', () => {
        this.isScrolling = true
      })
      this.$root.$on('onScroll', () => {
        this.isScrolling = true
      })
      this.$root.$on('onScrollEnd', () => {
        this.isScrolling = false
      })

      this.$root.$emit('app:created', this)
    },
    mounted () {
      // 设置当前可点击
      this.isClickBlockEnabled = true

      // for debug
      if (window.VM) {
        window.VM.$app = this
        window.VM.$root = this.$root
      }
      this.$root.$emit('app:mounted', this)
    }
  }
</script>