article-intro.vue 5.88 KB
<template>
  <div ref="intro" class="intro-wrap" :class="introClass" :style="introStyle" @click="onExpanding">
    <div class="title">{{this.data.articleTitle}}</div>
    <div v-if="trimIntro" class="intro-content">
      <p class="pre-wrap" @click="toUserPage" v-html="intro"></p>
      <div ref="introPool" class="intro-pool pre-wrap" v-if="!introHeight" v-html="trimIntro" >
      </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;
    },
    trimIntro() {
      keys(this.data.atUserInfo).forEach(k => {
        this.data.intro = this.data.intro.replace(new RegExp(`@${k}#`, 'gm'), gMentionsLink(k, this.data.atUserInfo[k]));
      });
      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;
    },
    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);

  .title {
    font-size: 36px;
    color: #222;
    letter-spacing: 0.34px;
    line-height: 56px;
  }

  .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>