widget-icon-btn.vue 6.66 KB
<template>
  <AuthComponent :auth="isAuth" class="icon-btn" @click="onClick" :style="btnStyle">
    <i class="iconfont" :class="iconClass" :style="iconStyle"></i>
    <p v-if="viewText" class="icon-btn-text" :style="textStyle">
      <span class="view-text">{{Number(viewText) ? viewText : ''}}</span>
      <span class="placeholder-text">{{placText}}</span>
    </p>
  </AuthComponent>
</template>

<script>
import {forEach, fill} from 'lodash';
import {createNamespacedHelpers} from 'vuex';
const {mapActions} = createNamespacedHelpers('user');
const {mapActions: articleMapActions} = createNamespacedHelpers('article');

const classMap = {
  fav: {
    default: 'icon-zan',
    selected: 'icon-zan-fill'
  },
  star: {
    default: 'icon-star',
    selected: 'icon-star-fill'
  },
  share: {
    default: 'icon-share'
  },
  msg: {
    default: 'icon-msg'
  }
};

const defaultOption = {
  canSelect: true,                  // 是否支持选中
  selected: false,                  // 初始选中状态(不受是否支持选中控制)
  color: '#444',                    // btn字体颜色
  selectedColor: '#d90025',         // btn选中状态字体颜色(不设置默认与非选中一致)
  iconFontSize: 48,                 // icon字号(单位px)
  textSelectedColor: '#444',        // text选中状态字体颜色(不设置默认与selectedColor一致)
  textFontSize: 20,                 // text字号(单位px)
  textAlign: 'top',                 // text位置, 默认normal(支持normal, top, bottom)
  textZoom: 0.9,                    // text缩放
  textAutoChange: true,            // text自动增减,只支持number类型(受是否支持选中控制)
  emitName: ''                      // 点击触发事件名称
};

export default {
  name: 'WidgetIconBtn',
  props: {
    type: {
      type: String,
      default: 'fav'
    },
    text: {
      type: [String, Number],
      default() {
        return '';
      }
    },
    articleId: Number,
    commentId: Number,
    option: {
      type: Object,
      default() {
        return defaultOption;
      }
    }
  },
  data() {
    return {
      viewOption: {},
      btnSelected: false,
      actionClass: '',
      editText: null
    };
  },
  created() {
    forEach(defaultOption, (value, key) => {
      this.viewOption[key] = this.option.hasOwnProperty(key) ? this.option[key] : defaultOption[key];
    });
    this.viewOption = {...this.viewOption};
  },
  computed: {
    syncFnName() {
      if (this.articleId) {
        if (this.type === 'fav') {
          return 'praiseArticle';
        } else if (this.type === 'star') {
          return 'followArticle';
        }
      } else if (this.commentId && this.type === 'fav') {
        return 'praiseComment';
      }
    },
    isAuth() {
      return !!this.syncFnName;
    },
    btnStyle() {
      let color = this.viewOption.color || defaultOption.color;

      return `color: ${this.btnSelected ? (this.viewOption.selectedColor || color) : color};`;
    },
    iconClass() {
      if (this.actionClass) {
        return this.actionClass;
      }
      if (classMap[this.type]) {
        const currentClass = classMap[this.type];

        if (this.viewOption.selected) {
          this.btnSelected = true;

          return currentClass.selected || currentClass.default;
        }

        return currentClass.default;
      }
    },
    iconStyle() {
      return `font-size: ${this.pxToRem(this.viewOption.iconFontSize)};`;
    },
    textStyle() {
      let style = `font-size: ${this.pxToRem(this.viewOption.textFontSize)};`;

      let textAlign = this.viewOption.textAlign;

      if (['top', 'bottom'].indexOf(textAlign) >= 0) {
        style += ` vertical-align: ${textAlign};`;
      }

      let textZoom = this.viewOption.textZoom;

      if (Number(textZoom) !== NaN) {
        style += ` transform: scale(${textZoom}, ${textZoom});`;
      }

      if (this.viewOption.textSelectedColor) {
        style += ` color: ${this.viewOption.textSelectedColor};`;
      }

      return style;
    },
    viewText() {
      return `${(this.editText === null ? this.text : this.editText)}`;
    },
    placText() {
      if (!isNaN(Number(this.text)) && this.viewText.length) {
        return fill(Array(this.viewText.length), 0).join('');
      } else  {
        return `${this.text}`;
      }
    }
  },
  methods: {
    ...mapActions(['followArticle', 'praiseArticle', 'praiseComment']),
    ...articleMapActions(['fetchArticleUpdate']),
    pxToRem(px) {
      const rootValue = 40;

      if (typeof px !== 'number') {
        px = parseInt(`0${px}`, 10);
      }

      if (px > 2) {
        return (px / rootValue).toFixed(2) + 'rem';
      } else {
        return px + 'px';
      }
    },
    changeBtnStatus() {
      this.btnSelected = !this.btnSelected;

      if (this.viewOption.textAutoChange) {
        if (!isNaN(Number(this.viewText))) {
          this.editText = Number(this.viewText) + (this.btnSelected ? 1 : -1);
        }
      }

      if (classMap[this.type]) {
        this.actionClass = this.btnSelected ? classMap[this.type].selected : classMap[this.type].default;
      }
    },
    syncService(type, data) {
      if (typeof this[type] === 'function') {
        return this[type](data);
      } else {
        return Promise.resolve({code: 404});
      }
    },
    onClick(evt) {
      if (this.syncing) {
        return;
      }

      if (this.viewOption.canSelect) {
        this.changeBtnStatus();

        if (this.syncFnName) {
          const backFn = (res) => {
            this.syncing = false;

            if (res.code !== 200) {
              this.changeBtnStatus();
              this.$createToast && this.$createToast({
                txt: res.message || '服务器开小差了',
                type: 'warn',
                time: 1000
              }).show();
            } else {
              if (['praiseArticle', 'followArticle'].some(s => s === this.syncFnName)) {
                this.fetchArticleUpdate({articleId: this.articleId});
              }
            }
          };

          this.syncing = true;

          this.syncService(this.syncFnName, {
            articleId: this.articleId,
            commentId: this.commentId,
            status: this.btnSelected
          }).then(backFn).catch(backFn);
        }
      }

      this.viewOption.emitName && this.$emit(this.viewOption.emitName, evt);
    }
  },
};
</script>

<style type="scss">
  .icon-btn {
    display: inline-block;
    line-height: 1;
    vertical-align: middle;

    > * {
      display: inline-block;
      vertical-align: middle;
    }

    .icon-btn-text {
      position: relative;

      .view-text {
        position: absolute;
      }

      .placeholder-text {
        opacity: 0;
      }
    }
  }
</style>