|                                                                                                                                                                                              |  | <template>  <view :class="['product', size]"     @touchstart="onTouchstart"     @touchmove="onTouchmove"     @touchend="onTouchend"  >    <image class="product-img" :src="data.image" mode="aspectFill"></image>    <view class="flex flex-column product-info">      <view class="product-info-top">        <view class="product-name text-ellipsis-2">{{ data.title }}</view>        <view class="product-desc text-ellipsis" v-if="tagDesc">{{ tagDesc }}</view>      </view>      <view class="flex product-info-bottom">        <view class="product-detail">          <view class="flex product-price">            <view class="product-price-val">              <text>¥</text>              <text class="highlight">{{ priceInt }}</text>              <text>{{ `${priceFrac}起` }}</text>            </view>            <view class="product-price-bef" v-if="data.priceOrigin">              {{ `¥${data.priceOrigin}` }}            </view>          </view>          <view class="product-registered">            {{ `${data.applyNum}人已报名` }}          </view>        </view>        <button class="btn" @click="onRegistrate">报名</button>      </view>    </view>
    <button class="flex btn btn-collect"       :style="collectBtnStyle"      @click.stop="onCollect"      @touchstart.stop="onCollect"    >      <view>{{ isCollected ? '移除收藏' : '收藏' }}</view>    </button>  </view></template>
<script>  export default {    props: {      data: {        type: Object,        default() {          return {}        }      },      size: {        type: String,        default: 'normal' // normal | small
      }    },    data() {      return {        isMove: false,        startClientX: null,        displayX: 0,        collectBtnVisible: false,      }    },    computed: {      tagDesc() {        const { tagList } = this.data
        return tagList?.length ? tagList.split('、').join('·') : ''      },      priceInt() {        return Math.floor(this.data.priceDiscount)      },      priceFrac() {        let frac = this.data.priceDiscount % this.priceInt        return frac > 0 ? frac.toFixed(2).slice(1) : ''      },      isCollected() {        return this.data.isCollection == '1'      },      collectBtnWidth() {        return this.isCollected ? 80 : 56      },      collectBtnStyle() {        const width = this.collectBtnWidth        const background = this.isCollected ? '#26334E' : '#FF9035'
        let display = Math.ceil(this.displayX / width * 100)
        display > 100 && (display = 100)
        const translateX = 100 - display
        return `width: ${width}px; transform: translateX(${translateX}%); background: ${background};`      },    },    methods: {      onTouchstart(e) {        const clientX = e.changedTouches[0].clientX
        this.isMove = false        this.startClientX = clientX        this.displayX = 0      },      onTouchmove(e) {        const clientX = e.changedTouches[0].clientX
        if (clientX < this.startClientX) {          this.displayX = this.startClientX - clientX        } else {          this.displayX = 0        }
        this.isMove = true      },      onTouchend() {
        if (!this.isMove && !this.collectBtnVisible) {          this.onRegistrate()        }
        if (this.displayX < this.collectBtnWidth) {          this.displayX = 0        }
        if (this.displayX) {          this.collectBtnVisible = true        } else {          this.collectBtnVisible = false        }
        this.isMove = false      },      showCollectBtn() {        this.displayX = 100      },      hiddenCollectBtn() {        this.displayX = 0      },      async onCollect() {
        try {
          let succ = await this.$store.dispatch('collect', this.data.id)          succ && this.hiddenCollectBtn()
          this.$emit('collect', !this.isCollected)        } catch (err) {          console.log('collect err', err)        }
      },      onRegistrate() {        this.$utils.navigateTo(`/pages_order/product/productDetail?id=${this.data.id}`)      },    },  }</script>
<style scoped lang="scss">  .product {    position: relative;    height: 464rpx;    background: #FFFFFF;    border: 2rpx solid #FFFFFF;    border-radius: 32rpx;    overflow: hidden;    font-size: 0;
    &-img {      width: 100%;      height: 220rpx;    }
    &-info {      height: 244rpx;      padding: 16rpx 16rpx 24rpx 16rpx;      box-sizing: border-box;      justify-content: space-between;
      &-top {        width: 100%;      }
      &-bottom {        width: 100%;        justify-content: space-between;      }
    }
    &-name {      font-size: 28rpx;      font-weight: 500;      color: #000000;    }
    &-desc {      margin-top: 8rpx;      font-size: 24rpx;      color: #8B8B8B;    }
    &-detail {
    }        &-price {      justify-content: flex-start;      align-items: baseline;      column-gap: 12rpx;      flex-wrap: wrap;
      &-val {        font-size: 24rpx;        font-weight: 500;        color: #FF4800;        white-space: nowrap;
        .highlight {          font-size: 32rpx;        }
      }
      &-bef {        text-decoration: line-through;        font-size: 24rpx;        color: #8B8B8B;      }    }
    &-registered {      font-size: 24rpx;      color: #8B8B8B;    }
    .btn {      padding: 11rpx 32rpx;      font-size: 26rpx;      font-weight: 500;      color: #FFFFFF;      background: #00A9FF;      border-radius: 24rpx;      white-space: nowrap;    }
    &.small {      .btn {        padding: 11rpx 16rpx;      }    }
  }
  .btn.btn-collect {    position: absolute;    top: 0;    right: 0;    row-gap: 8rpx;    // width: 112rpx;
    height: 100%;    font-size: 24rpx;    line-height: 1;    color: #FFFFFF;    // background: #FF9035;
    border-radius: 0;  }
</style>
 |