article-intro.vue 6.38 KB
<template>
  <div ref="intro" class="intro-wrap" :class="introClass" :style="introStyle" @click="onExpanding">
    <div class="context-title">{{this.data.articleTitle}}</div>
    <div v-if="trimIntro" class="intro-content">
      <p class="pre-wrap" @click="toUserPage" v-html="introDecorator"></p>
      <div ref="introPool" class="intro-pool pre-wrap" v-if="!introHeight" v-html="trimIntroDecorator" >
      </div>
      <span ref="expand" class="expand" v-if="showExpandTxt">…<b>继续阅读</b></span>
    </div>
    <div class="collapse" v-if="showCollapseTxt">收起</div>
  </div>
</template>

<script>
import {get, trim, forEach, keys} from 'lodash';
import YAS from 'utils/yas-constants';

const MAX_LINES = 6;

const gMentionsLink = function(key, name) {
  return `<span style="color:#1890ff" id='${key}'>@${name}</span>`;
};

export default {
  name: 'ArticleIntro',
  props: {
    data: {
      type: Object,
      default() {
        return {};
      }
    },
    share: Boolean,
    thumb: Boolean,
    posId: Number,
  },
  data() {
    return {
      introHeight: 0,
      introCollapseHeight: 0,
      isIntroEllipsis: true,
      isIntroExpand: false,
      isIntroAnimateing: false,
      canRetract: false,
    };
  },
  watch: {
    'data.intro': function() {
      this.introHeight = 0;
      this.introCollapseHeight = 0;
      this.updateHeight();
    }
  },
  computed: {
    maxLines() {
      return (this.data.maxLines || MAX_LINES) - 1;
    },
    trimIntroDecorator() {
      let _ = this.trimIntro;

      keys(this.data.atUserInfo).forEach(k => {
        _ = _.replace(new RegExp(`@${k}#`, 'gm'), gMentionsLink(k, this.data.atUserInfo[k]));
      });
      return _;
    },
    trimIntro() {
      return trim(this.data.intro);
    },
    showIntro() {
      let line = 0;

      let textArr = [];

      forEach(this.trimIntro, (val) => {
        let isEnter = /(\r\n)|(\n)/.test(val);

        textArr[line] = textArr[line] || '';
        textArr[line] += val;

        if (textArr[line].length >= 21 || isEnter) {
          line++;
        }

        if (line > this.maxLines) {
          textArr[this.maxLines] = textArr[this.maxLines].substring(0, Math.min(17, textArr[this.maxLines].length));
          return false;
        }
      });

      this.isIntroEllipsis = line > this.maxLines;

      let info = trim(textArr.join(''));

      keys(this.data.atUserInfo).forEach(k => {
        info = info.replace(new RegExp(`@${k}#`, 'gm'), gMentionsLink(k, this.data.atUserInfo[k]));
      });

      return info;
    },
    introDecorator() {
      let _ = this.intro;

      keys(this.data.atUserInfo).forEach(k => {
        _ = _.replace(new RegExp(`@${k}#`, 'gm'), gMentionsLink(k, this.data.atUserInfo[k]));
      });
      return _;
    },
    intro() {
      if (this.isIntroExpand || !this.isIntroEllipsis) {
        return this.trimIntro;
      } else {
        return this.showIntro;
      }
    },
    introClass() {
      return {
        'intro-expand': this.isIntroExpand,
        'no-more': !this.isIntroEllipsis,
        'intro-will-change': this.isIntroAnimateing
      };
    },
    introStyle() {
      let introHeight;

      if (this.isIntroExpand) {
        introHeight = this.introHeight;
      } else {
        introHeight = this.introCollapseHeight;
      }

      return {
        height: introHeight ? `${introHeight}px` : void 0
      };
    },
    showExpandTxt() {
      return this.isIntroEllipsis && !this.isIntroExpand && !this.isIntroAnimateing;
    },
    showCollapseTxt() {
      return this.isIntroEllipsis && this.isIntroExpand && !this.isIntroAnimateing && this.canRetract;
    }
  },
  mounted() {
    this.updateHeight();

    this.$refs.intro && this.$refs.intro.addEventListener('transitionend', this.onExpand.bind(this));
  },
  destroyed() {
    this.$refs.intro && this.$refs.intro.removeEventListener('transitionend', this.onExpand.bind(this));
  },
  methods: {
    toUserPage(e) {
      if (e.target.id ?? false) {
        this.$router.push({
          name: 'author',
          params: {
            type: this.data.authorType,
            id: e.target.id
          }
        });
      }
      this.reportClickAvatar();
    },
    reportClickAvatar() {
      this.$store.dispatch('reportYas', {
        params: {
          appop: YAS.eventName.avatar,
          param: {
            AUTH_ID: this.data.authorUid,
            POS_ID: this.posId
          }
        }
      });
    },
    updateHeight() {
      this.$nextTick(() => {
        this.introHeight = get(this.$refs, 'introPool.scrollHeight', 0) + 22;
        this.introCollapseHeight = get(this.$refs, 'intro.offsetHeight', 0);
      });
    },
    onExpanding() {
      if (!this.isIntroEllipsis || (this.isIntroExpand && !this.canRetract)) {
        return;
      }

      if (!this.canRetract) {
        this.canRetract = this.introHeight - 23 > get(this.$refs, 'expand.offsetHeight', 0) * (this.maxLines + 4);
      }

      this.isIntroExpand = !this.isIntroExpand;
      this.isIntroAnimateing = true;
    },
    onExpand() {
      this.isIntroAnimateing = false;
      this.$nextTick(() => {
        this.$emit('on-expand');
      });
    },
  },
};
</script>

<style lang="scss" scoped>
  .intro-wrap {
    padding: 0 30px;
    margin-top: 20px;
    overflow: hidden;
    transition: height 250ms cubic-bezier(0.165, 0.84, 0.44, 1);

    .context-title {
      font-size: 36px;
      color: #222;
      line-height: 56px;
      margin-top: 10px;
      margin-bottom: 16px;
      letter-spacing: 0.68px;
      font-weight: bolder;
    }

    .intro-content {
      line-height: 1.7;
      font-size: 32px;
      color: #4a4a4a;
      box-sizing: content-box;
      position: relative;
    }

    .pre-wrap {
      white-space: pre-wrap;
      word-wrap: break-word;
      display: inline;
    }

    &.intro-will-change {
      will-change: height;
    }

    &.intro-expand {
      -webkit-line-clamp: initial;
      text-overflow: initial;
    }

    &.no-more {
      height: auto;
    }
  }

  .intro-pool {
    position: absolute;
    width: 100%;
    top: -1000px;
    visibility: hidden;
  }

  .collapse {
    width: 100%;
    text-align: right;
    font-size: 32px;
    color: #000;
    font-weight: bold;
  }

  .expand {
    font-size: 32px;
    color: #000;
    line-height: 20px;
    white-space: nowrap;

    &.collapse {
      position: absolute;
      right: 14px;
      bottom: 0;
      height: 28px;
    }

    > b {
      font-weight: bold;
    }
  }
</style>