自定义简单的自动滚动列表,可加样式

0 阅读5分钟

大概有以下样式

1.基础斑马纹

image.png

2.固定状态栏 传参为(type: 'recoverState')

image.png

3.进度条 传参为

field-infos="[
        { key: 'name', label: 'XXXXX' },
        { key: 'voltage_level', label: 'XXXXX' },
        { key: 'active_power', label: 'XXXXX',tableUnit: '单位1' },
        { key: 'rated_power', label: 'XXXXX' ,tableUnit: '单位2'},
        { key: 'load_rate', label: 'XXXXX', type: 'progress', unit: '%', color: COLOR_DARK_RED },
      ]"

image.png

全部代码如下

import { defineComponent } from 'vue'
const mockData = [
  {
    name: '北京',
    ratio: +2.3,
    value: 178,
  },
  {
    name: '天津',
    ratio: -0.2,
    value: 134,
  },
  {
    name: '武汉',
    ratio: -1.53,
    value: 111,
  },
  {
    name: '合肥',
    ratio: 0.23,
    value: 80,
  },
  {
    name: '南京',
    ratio: 1.7,
    value: 500,
  },
  {
    name: '武汉',
    ratio: -1.53,
    value: 111,
  },
  {
    name: '合肥',
    ratio: 0.23,
    value: 80,
  },
  {
    name: '南京',
    ratio: 1.7,
    value: 500,
  },
  {
    name: '北京',
    ratio: +2.3,
    value: 178,
  },
  {
    name: '天津',
    ratio: -0.2,
    value: 134,
  },
  {
    name: '武汉',
    ratio: -1.53,
    value: 111,
  },
  {
    name: '合肥',
    ratio: 0.23,
    value: 80,
  },
  {
    name: '南京',
    ratio: 1.7,
    value: 500,
  },
  {
    name: '武汉',
    ratio: -1.53,
    value: 111,
  },
  {
    name: '合肥',
    ratio: 0.23,
    value: 80,
  },
  {
    name: '南京',
    ratio: 1.7,
    value: 500,
  },
]

const fieldInfos = [
  {
    key: 'name',
    label: '城市',
    type: '',
    unit: '',
  },
  {
    key: 'ratio',
    label: '同比',
    type: '',
    unit: '',
    color: '#02e3f4',
  },
  {
    key: 'value',
    label: '金额',
    type: '',
    unit: '',
    color: '#02e3f4',
  },
]
const TABS = ['斑马纹', '底部边框']
const props = {
  // 数据
  data: {
    type: Array,
    required: true,
    default: () => mockData,
  },
  // 字段信息
  fieldInfos: {
    type: Array,
    default: () => fieldInfos,
  },
  tableStyle: {
    type: String,
    default: TABS[0],
  },
  rowHeight: {
    type: Number,
    default: 112,
  },
  TableHeaderHeight: {
    type: Number,
    default: 112,
  },
  // 显示表头
  showTableHeader: {
    type: Boolean,
    default: true,
  },
  autoScroll: {
    type: Boolean,
    default: true,
  },
  scrollSize: {
    type: Number,
    default: 1,
  },
  showTableBorder: {
    type: Boolean,
    default: false,
  },
  lazy: {
    type: Boolean,
    default: false,
  },
  currentIndex: {
    type: Number,
    default: -1,
  },
}

export default defineComponent({
  props: props,
  data() {
    return {
      // currentIndex: -1,
      items: this.data,
      isScrolling: false,
      scrollInterval: null,
      animationFrameId: null,
    }
  },
  computed: {
    totalRows() {
      return this.data.length
    },
  },
  watch: {
    autoScroll: {
      handler(newVal) {
        if (newVal) {
          this.startAutoScroll()
        } else {
          this.stopAutoScroll()
        }
      },
      immediate: true,
    },
  },
  mounted() {},
  beforeDestroy() {
    this.clearTimer()
    this.cancelAnimation()
  },
  methods: {
    change(index) {
      const sIndex = index === this.currentIndex ? -1 : index
      this.$emit('change', sIndex)
    },
    // 向下滚动scrollSize行
    scrollDown() {
      if (this.isScrolling) return

      this.isScrolling = true

      const tbody = this.$refs.scrollRef
      if (!tbody) {
        this.isScrolling = false
        return
      }

      const scrollDistance = this.rowHeight * this.scrollSize
      const targetScrollTop = tbody.scrollTop + scrollDistance
      const maxScrollTop = tbody.scrollHeight - tbody.clientHeight

      // 如果到达底部,回到顶部
      let finalTarget
      if (targetScrollTop > maxScrollTop) {
        finalTarget = 0
      } else {
        finalTarget = targetScrollTop
      }

      this.smoothScrollTo(finalTarget)

      setTimeout(() => {
        this.isScrolling = false
      }, 300)
    },

    // 向上滚动scrollSize行
    scrollUp() {
      if (this.isScrolling) return

      this.isScrolling = true

      const tbody = this.$refs.scrollRef
      if (!tbody) {
        this.isScrolling = false
        return
      }

      const scrollDistance = this.rowHeight * this.scrollSize
      const targetScrollTop = tbody.scrollTop - scrollDistance
      const maxScrollTop = tbody.scrollHeight - tbody.clientHeight

      // 如果到达顶部,回到底部
      let finalTarget
      if (targetScrollTop < 0) {
        finalTarget = maxScrollTop
      } else {
        finalTarget = targetScrollTop
      }

      this.smoothScrollTo(finalTarget)

      setTimeout(() => {
        this.isScrolling = false
      }, 300)
    },

    // 平滑滚动到指定位置
    smoothScrollTo(targetScrollTop) {
      const tbody = this.$refs.scrollRef
      if (!tbody) return

      this.cancelAnimation()

      const startScrollTop = tbody.scrollTop
      const distance = targetScrollTop - startScrollTop
      const duration = 300
      let startTime = null

      const animateScroll = (timestamp) => {
        if (!startTime) startTime = timestamp
        const elapsed = timestamp - startTime
        const progress = Math.min(elapsed / duration, 1)

        // 使用缓动函数
        const easeProgress = 1 - Math.pow(1 - progress, 3)

        tbody.scrollTop = startScrollTop + distance * easeProgress

        if (progress < 1) {
          this.animationFrameId = requestAnimationFrame(animateScroll)
        } else {
          this.animationFrameId = null
        }
      }

      this.animationFrameId = requestAnimationFrame(animateScroll)
    },

    // 处理鼠标滚轮
    handleWheel(event) {
      event.preventDefault()

      const tbody = this.$refs.scrollRef
      if (!tbody) return

      // 使用原生滚动,不调用自动滚动的逻辑
      tbody.scrollTop += event.deltaY
    },

    // 开始自动滚动
    startAutoScroll() {
      this.clearTimer()
      this.scrollInterval = setInterval(() => {
        if (this.autoScroll) {
          this.scrollDown()
        }
      }, 2000) // 每2秒滚动一次
    },

    // 停止自动滚动
    stopAutoScroll() {
      this.clearTimer()
    },

    handleMouseover() {
      this.stopAutoScroll()
    },

    handleMouseout() {
      if (this.autoScroll) {
        this.startAutoScroll()
      }
    },

    clearTimer() {
      if (this.scrollInterval) {
        clearInterval(this.scrollInterval)
        this.scrollInterval = null
      }
    },

    cancelAnimation() {
      if (this.animationFrameId) {
        cancelAnimationFrame(this.animationFrameId)
        this.animationFrameId = null
      }
    },

    getGridColumns(type) {
      const widthList = this.fieldInfos.map((item) => {
        return item?.width || '1fr'
      })
      const gridColumns = widthList.join(' ')
      return `grid-template-columns:${gridColumns};height:${type ? this.TableHeaderHeight : this.rowHeight}px`
    },

    getColorByAlpha(color, alpha) {
      return echarts.color.modifyAlpha(color, alpha)
    },

    getColorByText(fieldItem, value) {
      if (fieldItem.customColor) {
        return fieldItem.customColor[value] || '#fff'
      }
      if (fieldItem.color) {
        return fieldItem.color
      }
      return '#fff'
    },
  },
})

<div
  class="commont-list"
  :class="{
    'commont-list__border': showTableBorder,
  }"
>
  <div v-show="showTableHeader" class="n-list__row n-list__thead" :style="getGridColumns('header')">
    <div v-for="(fieldItem, idx) in fieldInfos" :key="idx" class="n-list__item-container">
      {{ fieldItem.label }}
      <span v-if="fieldItem.tableUnit" style="font-size: 30px;margin-left: 10px;">({{ fieldItem.tableUnit }})</span>
    </div>
  </div>
  <div
    ref="scrollRef"
    :class="{ 'no-header': !showTableHeader }"
    class="n-list__tbody"
    :style="`height: calc(100% - ${TableHeaderHeight}px)`"
    @mouseover="handleMouseover"
    @mouseout="handleMouseout"
    @wheel.prevent="handleWheel"
  >
    <div
      v-for="(item, index) in data"
      :key="index"
      class="n-list__row"
      :class="{
        selected: index === currentIndex,
        'n-list__row__border': tableStyle === '底部边框',
        'n-list__row__zebra': tableStyle === '斑马纹',
      }"
      :style="getGridColumns()"
      @click="change(index)"
    >
      <div v-for="(fieldItem, idx) in fieldInfos" :key="idx" class="n-list__item-container">
        <template v-if="fieldItem.type === 'order'">
          <!-- 序号 -->
          <div
            class="n-list__item n-list__item-rank"
            :style="{ color: fieldItem?.color || '#fff', Height: rowHeight + 'px' }"
          >
            {{ index + 1 }}
          </div>
        </template>

        <template v-else-if="fieldItem.type === 'progress'">
          <!-- 进度条 -->
          <div
            class="n-list__item n-list__item-progress"
            :style="{ color: fieldItem?.color || '#fff', Height: rowHeight + 'px' }"
          >
            <el-progress
              :stroke-width="50"
              :percentage="+item[fieldItem.key]"
              :style="{ color: fieldItem?.color || '#fff' }"
              :color="getColorByAlpha(fieldItem.color, 0.4)"
            ></el-progress>
          </div>
        </template>

        <template v-else-if="fieldItem.type === 'recoverState'">
          <!-- 恢复状态 -->
          <div class="n-list__item n-list__item-recover-state">
            <general-indicator-card3
              aria-label="指标卡片3"
              :recover-state="item[fieldItem.key]"
              class="boxs"
            ></general-indicator-card3>
          </div>
        </template>

        <template v-else>
          <!-- 普通文本 -->
          <div class="n-list__item" :style="{ color: getColorByText(fieldItem, item[fieldItem.key]) }">
            <!-- 文本 -->
            <span class="n-list__item-text" :title="item[fieldItem.key]">
              {{ item[fieldItem.key] }}{{ fieldItem.unit }}
            </span>
          </div></template
        >
      </div>
    </div>
  </div>
</div>


.commont-list {
  width: 100%;
  height: 100%;
  letter-spacing: 0;
  --rank-background-image: none;
  --rank-background-size: 100% 100%;
  --thead-color: var(--g-text-color);
  --thead-bgcolor: rgba(255, 255, 255, 0.08);
  --thead-fontsize: var(--g-text-fontsize);
  --thead-fontweight: bold;
  --margin-bottom: 0em;
  --text-color: var(--g-text-color);
  --text-fontsize: var(--g-text-fontsize);
  --text-fontweight: normal;
  --border-width: 0em 0em 0.0625em;
  --border-style: solid;
  --border-color: rgba(255, 255, 255, 0.1);
  --line-bgcolor: rgba(137, 194, 251, 0.15);
  --selected-bgcolor: rgba(0, 0, 0, 0);
  box-sizing: content-box;

  .text-hidden {
    overflow: hidden;
    white-space: nowrap;
  }

  .text-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .text-ellipsis {
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }

  .n-list__row {
    width: 100%;
    display: grid;
    line-height: 100%;
    grid-template-columns: repeat(4, 1fr);

    .n-list__item-container {
      text-align: center;

      .n-list__item {
        width: 100%;
        height: 100%;
        padding: 0 0.25em;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .n-list__item-rank {
        .text-hidden();
        .text-wrap();
        background-repeat: no-repeat;
        background-position: center;
      }

      .n-list__item-progress {
        .el-progress {
          // height: 60px;
          width: 85%;
          position: relative;

          .el-progress-bar__outer {
            border-radius: 0;
            background-color: rgba(255, 255, 255, 0.13) !important;
          }

          .el-progress-bar__inner {
            border-radius: 0;

            .el-progress-bar__innerText {
              font-size: 40px !important;
              font-family: 'TRENDS';
              color: inherit !important;
            }
          }
        }

        .el-progress__text {
          position: absolute;
          font-size: 40px !important;
          font-family: 'TRENDS';
          color: inherit !important;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
        }
      }

      .n-list__item-recover-state {
        .boxs {
          width: 322px;
          min-width: 222px;
          height: 70%;
          min-height: 60px;
        }
      }
      

      .n-icon {
        width: 100%;
        height: calc(100% - 0.375em);
        margin-top: 0.1875em;
        display: inline-block;
        background-position: center;
        background-repeat: no-repeat;
        background-size: contain;
      }
    }
  }

  .n-list__thead {
    color: var(--thead-color);
    font-size: 42px;
    background-color: var(--thead-bgcolor);
    font-weight: var(--thead-fontweight);
    position: relative;
    border-bottom: 1px solid rgba(134, 191, 250, 0.3);

    &::before,
    &::after {
      content: '';
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      width: 3px;
      height: 40%;
      /* 高度的30% */
      background-color: white;
    }

    &::before {
      left: 0;
    }

    &::after {
      right: 0;
    }

    .n-list__item-container {
      display: flex;
      align-items: center;
      justify-content: center;
    }
  }

  .n-list__tbody {
    color: var(--text-color);
    font-size: var(--text-fontsize);
    height: calc(100% - 112px);
    overflow-y: auto;
    overflow-x: hidden;
    scroll-behavior: smooth;
    font-weight: var(--text-fontweight);

    &::-webkit-scrollbar {
      width: 8px; // 滚动条宽度
    }

    &::-webkit-scrollbar-track {
      background: rgba(255, 255, 255, 0.10); // 滚动条轨道背景
      border-radius: 4px;
    }

    &::-webkit-scrollbar-thumb {
      background: rgba(2, 227, 244, 0.5); // 蓝色滚动条滑块
      border-radius: 4px;

      &:hover {
        background: rgba(255, 255, 255, 0.10); // 鼠标悬停时更深一点的蓝色
      }
    }

    &::-webkit-scrollbar-corner {
      background: rgba(255, 255, 255, 0.10); // 滚动条角落背景
    }

    &.no-header {
      height: 100%;
    }

    .touch-bottom {
      color: rgba(85, 85, 85, 0.5);
      text-align: center;
      font-size: 0.5em;
    }

    .n-list__row {
      border-style: var(--border-style);
      border-width: var(--border-width);
      border-color: var(--border-color);
      margin-bottom: var(--margin-bottom);
      cursor: pointer;

      &:last-child {
        margin-bottom: 0;
      }

      &.clickable {
        cursor: pointer;
      }

      &.selected {
        background-color: var(--selected-bgcolor);
      }

      .n-list__item-text {
        overflow: hidden;
        text-overflow: ellipsis;
        width: 100%;
        max-width: 900px;
      }
    }

    .n-list__row__zebra {

      // 奇数行透明,偶数行保持原有背景
      &:nth-child(odd) {
        background-color: transparent;
      }

      &:nth-child(even) {
        background-color: var(--line-bgcolor);
      }
    }

    .n-list__row__border {
      background-color: transparent;
      border-bottom: 1px solid rgba(134, 191, 250, 0.3);
    }

    .n-list__row.selected {
      background-color: rgba(82, 230, 255, 0.31);
      border: 1px solid #00FF7D;
    }
  }
}

.commont-list__border {
  border: 1px solid rgba(134, 191, 250, 0.3);
}