需求描述

  场景:现有很多类表单,进入表单详情后需要使用按钮提供表单的相关审批操作,需封装一个通用的按钮组件以满足不同表单不同需求操作

  原型图如下:

element ui封装按钮组件 vue封装按钮_前端

element ui封装按钮组件 vue封装按钮_加载_02

  思路:既然是在移动端,那我们是需要用到vant移动端组件库了。首先我们需要在项目中引入vant,然后绘制出原型图中的页面,最后考虑将其封装为一个通用组件。

步骤

引入vant

安装 npm install vant --save引入
1、 在main.js文件中引入

// 全局引入
import Vant from 'vant'
Vue.use(Vant)

2、按需引入

// 第一步--安装插件
npm i babel-plugin-import -D // 简写
npm install babel-plugin-import --save-dev // 完整写法
// 第二步--配置插件--在.babelrc 或 babel.config.js 中添加配置
"plugins": [
    [
      "import",
      {
        "libraryName": "vant",
        "libraryDirectory": "es",
        "style": true
      }
    ]
  ]
  // 第三步--按需引入vant组件--此处只作简单举例
  import { Button } from 'vant;
  Vue.use(Button)

使用

// 可直接在vue文件中使用啦
<van-button type="info">{{ 666 }}</van-button>
设计按钮组件

  通过分析原型图,我们可以确定,我们需要实现的按钮组件主要分为两部分:一个主按钮和其他按钮,其他按钮包含其他操作。关于布局我们可以采用栅栏布局,左右两边各自占有12列(一行共24列),按钮绘制代码如下:

// btnOptions为存放按钮参数的数组--此处给出一个模拟数组
/*
   btnOptions: [{
       text: '提交',
       value: 'submit',
       buttonCode: 'submit'
     }, {
       text: '同意',
       value: 'Approve',
       buttonCode: 'Approve'
     }, {
       text: '拒绝',
       value: 'Reject',
       buttonCode: 'Reject'
     }, {
       text: '返回',
       value: 'goBackPage',
       buttonCode: 'goBackPage'
     }]
*/
<van-row v-if="btnOptions.length > 0">
     <van-col span="12">
       <van-dropdown-menu
         v-if="option.length > 1"
         direction="up"
         active-color="#1989fa"
       >
         <van-dropdown-item ref="other" title="其他">
           <van-button
             v-for="item in option"
             :key="item.value"
             plain
             hairline
             type="default"
             block
             @click="btnClick(item)"
           >{{ item.text }}</van-button>
         </van-dropdown-item>
       </van-dropdown-menu>
       <template v-else>
         <van-button
           v-if="option.length > 0"
           type="default"
           block
           @click="btnClick(option[0])"
         >{{ option[0].text }}</van-button>
       </template>
     </van-col>
     <van-col span="12">
       <van-button type="info" block @click="btnClick(mainBtn)">{{ mainBtn.text }}</van-button>
     </van-col>
   </van-row>

  到这一步,我们就已经绘制出了如原型图中的按钮样式,效果如图:

element ui封装按钮组件 vue封装按钮_javascript_03


  我们发现,此时按钮展示效果已经有了,但是我们还需要将它固定在页面底部,我们给它一个样式class,定义为class="buttons",css代码具体如下:

<style lang="less" scoped>
.buttons {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  .info {
    padding: 10px;
  }
}
</style>

  效果如图:

element ui封装按钮组件 vue封装按钮_前端_04


  接下来,我们要为这些按钮事件写一下通用的代码逻辑,在前面绘制展示按钮时,我们已经定义了一个按钮点击方法btnClick(msg),具体代码如下:

// 按钮点击
    btnClick(msg) {
      if (msg.value === 'goBackPage') { // 返回
        this.$router.go(-1)
        return
      }
      this.loading = false // 控制加载动画
      this.btnType = msg
      if (this.$refs.other) {
        this.$refs.other.toggle(false)
      }
      if (msg.notDialog) {
        // 不显示弹窗
        this.deliverInfo()
      } else {
        this.show = true // 点击按钮弹出交互弹窗
        // 赋值提示信息
        if (msg.dialogInfo) {
          this.dialogInfo = msg.dialogInfo
        }
        if (msg.buttonCode === 'Reject' || msg.buttonCode === 'Approve') {
          this.dialogInfo = {
            title: '审批意见',
            type: 'input',
            info: ''
          }
        } else if (msg.buttonCode === 'approvalHistory' || msg.buttonCode === 'saveTemplate') {
          this.show = false
          this.deliverInfo()
        } else {
          this.dialogInfo = {
            title: '提示',
            type: 'text',
            info: '确认进行该操作吗?'
          }
        }
      }
    },
    // 传递信息
    deliverInfo() {
      if (this.loading) {
        return
      }
      if (this.btnType.buttonCode !== 'approvalHistory' && this.btnType.buttonCode !== 'saveTemplate') {
        this.loading = true
        Toast.loading({
          message: '加载中...',
          forbidClick: true,
          overlay: true,
          duration: 30000 // 默认加载时间--可用作超时处理
        })
        this.loading = true
      }
      this.$emit('btnClick', { ...this.dialogInfo, ...this.btnType })
    }

  添加弹窗组件,代码如下:

<van-dialog
      v-model="show"
      width="80%"
      :title="dialogInfo.title"
      show-cancel-button
      confirm-button-color="#1989fa"
      @cancel="cancel"
      @confirm="confirm"
    >
      <div v-if="dialogInfo.type === 'text'" class="info">
        {{ dialogInfo.info }}
      </div>
      <van-field
        v-if="dialogInfo.type === 'input'"
        v-model="dialogInfo.info"
        rows="4"
        autosize
        type="textarea"
        maxlength="300"
        show-word-limit
      />
    </van-dialog>

  点击提交按钮与同意效果如下(左:‘提交’,右:‘同意’):

element ui封装按钮组件 vue封装按钮_javascript_05

element ui封装按钮组件 vue封装按钮_element ui封装按钮组件_06


  加载效果如图:

element ui封装按钮组件 vue封装按钮_element ui封装按钮组件_07


  刚刚我们在上面的代码中提到了加载效果的默认加载时间,但是在实际业务需求中,我们应该是需要通过业务逻辑来控制加载效果关闭的,如接口调用完毕,页面跳转加载完毕后再关闭加载效果,防止按钮重复点击造成接口多次调用等问题。我们需要再写一个方法来控制加载效果的关闭。具体代码如下:

// 按钮事件结束的回调
  cancelLoad(time = 1000) {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = null
    }
    this.timer = setTimeout(() => {
      this.loading = false
      clearTimeout(this.timer)
      this.timer = null
      Toast.clear()
    }, time)
  }

  当需要结束加载效果时,调用这个方法即可。
  到这里,我们已经基本完成了按钮组件的开发工作。但是,这个组件还不能被其他组件所使用,我们还需要稍微改造一下数据源—btnOptions,这个数组是存放按钮参数的。所以我们要封装按钮组件,就需要将这个数组作为接收数组,其他组件调用我们的按钮组件时就需要传递存放了按钮参数的数组。代码如下:

// 父组件
<buttons ref="btn" :btnOptions="btnList" @btnClick="btnClick"></buttons>
import Buttons from '按钮组件路径'
components: {
  Buttons
},
data() {
  return {
  	// 此数组来源可通过任意方式,如自定义、接口返回等
  	btnList: [{
       text: '提交',
       value: 'submit',
       buttonCode: 'submit'
     }, {
       text: '同意',
       value: 'Approve',
       buttonCode: 'Approve'
     }, {
       text: '拒绝',
       value: 'Reject',
       buttonCode: 'Reject'
     }, {
       text: '返回',
       value: 'goBackPage',
       buttonCode: 'goBackPage'
     }] 
  }
},

// 按钮组件
props: {
    btnOptions: {
      type: Array,
      default: function() {
        return []
      }
    }
},

  当其他组件想要调用按钮组件内的方法如关闭加载效果方法cancelLoad()时,可以通过this.$refs.btn.cancelLoad()来调用此方法。或者通过公共事件$bus也可以。
  到这里,一个完整的按钮组件就开发好啦,能够完美契合文章开头的需求场景。完整代码如下:

<!-- 此为按钮组件 -->
<template>
  <div class="buttons" safe-area-inset-bottom>
    <van-row v-if="btnOptions.length > 0">
      <van-col span="12">
        <van-dropdown-menu
          v-if="option.length > 1"
          direction="up"
          active-color="#1989fa"
        >
          <van-dropdown-item title="其他" ref="other">
            <van-button
              v-for="item in option"
              :key="item.value"
              plain
              hairline
              type="default"
              block
              @click="btnClick(item)"
              >{{ item.text }}</van-button
            >
          </van-dropdown-item>
        </van-dropdown-menu>
        <template v-else>
          <van-button
            v-if="option.length > 0"
            type="default"
            block
            @click="btnClick(option[0])"
            >{{ option[0].text }}</van-button
          >
        </template>
      </van-col>
      <van-col span="12">
        <van-button type="info" block @click="btnClick(mainBtn)">{{
          mainBtn.text
        }}</van-button>
      </van-col>
    </van-row>
    <van-dialog
      width="80%"
      v-model="show"
      :title="dialogInfo.title"
      show-cancel-button
      confirm-button-color="#1989fa"
      @cancel="cancel"
      @confirm="confirm"
    >
      <div class="info" v-if="dialogInfo.type === 'text'">
        {{ dialogInfo.info }}
      </div>
      <van-field
        v-if="dialogInfo.type === 'input'"
        v-model="dialogInfo.info"
        rows="4"
        autosize
        type="textarea"
        maxlength="300"
        show-word-limit
      />
    </van-dialog>
  </div>
</template>

<script>
import { Toast } from 'vant'
export default {
  props: {
    btnOptions: {
      type: Array,
      default: function() {
        return []
      }
    }
  },
  data() {
    return {
      loading: false, // 加载中
      show: false, // 弹窗提示
      timer: null, // 定时器
      dialogInfo: {},
      btnType: {} // 当前点击的按钮类型
    }
  },
  created() {
  },
  computed: {

    mainBtn() {
      const [main, ...surplus] = this.btnOptions
      return main
    },
    option() {
      const [main, ...surplus] = this.btnOptions
      return surplus
    },
    value() {
      return this.option[0].value
    }
  },
  mounted() {
  	// 此处可用公共事件监听加载效果关闭方法
    this.$bus.$on('closeLoading', res => {
      if (res === true) {
        this.cancelLoad()
      }
    })
  },
  methods: {
    // 按钮点击
    btnClick(msg) {
      if (msg.value === 'goBackPage') {
        this.$router.go(-1)
        return
      }
      this.loading = false
      this.btnType = msg
      if (this.$refs.other) {
        this.$refs.other.toggle(false)
      }
      if (msg.notDialog) {
        // 不显示弹窗
        this.deliverInfo()
      } else {
        this.show = true
        // 赋值提示信息
        if (msg.dialogInfo) {
          this.dialogInfo = msg.dialogInfo
        }
        if (msg.buttonCode === 'Reject' || msg.buttonCode === 'Approve') {
          this.dialogInfo = {
            title: '审批意见',
            type: 'input',
            info: ''
          }
        } else if (msg.buttonCode === 'approvalHistory' || msg.buttonCode === 'saveTemplate') {
          this.show = false
          this.deliverInfo()
        } else {
          this.dialogInfo = {
            title: '提示',
            type: 'text',
            info: '确认进行该操作吗?'
          }
        }
      }
    },
    // 弹窗取消事件
    cancel() {
      this.show = false
      if (this.dialogInfo.type === 'input') {
        // 清空输入框内容
        this.dialogInfo.info = ''
      }
    },
    // 弹窗确认事件
    confirm() {
      this.show = false
      this.deliverInfo()
    },
    // 传递信息
    deliverInfo() {
      if (this.loading) {
        return
      }
      if (this.btnType.buttonCode !== 'approvalHistory' && this.btnType.buttonCode !== 'saveTemplate') {
        this.loading = true
        Toast.loading({
          message: '加载中...',
          forbidClick: true,
          overlay: true,
          duration: 30000
        })
        this.loading = true
      }
      this.$emit('btnClick', { ...this.dialogInfo, ...this.btnType })
    },
    // 按钮事件结束的回调
    cancelLoad(time = 1000) {
      if (this.timer) {
        clearTimeout(this.timer)
        this.timer = null
      }
      this.timer = setTimeout(() => {
        this.loading = false
        clearTimeout(this.timer)
        this.timer = null
        Toast.clear()
      }, time)
    }
  }
}
</script>

<style lang="less" scoped>
.buttons {
  position: fixed;
  bottom: -4px;
  left: 0;
  width: 100%;
  .info {
    padding: 10px;
  }
}
</style>

<!-- 此为父组件 -->
<template>
  <div>
    <buttons :btnOptions="btnOption" />
  </div>
</template>

<script>
import Buttons from '@/components/buttons/buttons.vue'
export default {
  name: '',
  components: {
    Buttons
  },
  data() {
    return {
      btnOption: [{
        text: '提交',
        value: 'submit',
        buttonCode: 'submit'
      }, {
        text: '同意',
        value: 'Approve',
        buttonCode: 'Approve'
      }, {
        text: '拒绝',
        value: 'Reject',
        buttonCode: 'Reject'
      }, {
        text: '返回',
        value: 'goBackPage',
        buttonCode: 'goBackPage'
      }]
    }
  },
  computed: {},
  watch: {},
  mounted() {},
  created() {},
  methods: {}
}

</script>