关于uniapp 中view循环中 超出文本跑马灯代码示例

93 阅读3分钟

某个公共组件的

<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { formatTime } from '@/utils'

const props = defineProps({
  argList: {
    type: Array,
    default: () => [{ name: '--', value: '--', unit: '--', url: '' }],
  },
  noteName:{
    type: String
  }
})

const emit = defineEmits<{
  (event: 'toDeviceDetail', item: { value: string | number; unit: string; name: string }): void
}>()

const tickerArr = ref([])

// 存储当前显示的值的索引
const displayValues = ref<
  Record<number, { current: string | number; prev: string | number; showPrev: boolean }>
>({})
// 新增:用于强制刷新的计数器
const refreshCounter = ref(0)
// 初始化 displayValues
onMounted(() => {
  props.argList.forEach((item, index) => {
    displayValues.value[index] = {
      current: item.value,
      prev: item.prevValue,
      showPrev: false,
    }
  })
  // 每秒递增计数器,触发视图更新
  const timer = setInterval(() => refreshCounter.value++, 1000)
  const instance = ref(getCurrentInstance())


  nextTick(()=>{
    // 先尝试从本地读取缓存
    const cachedWidth = uni.getStorageSync('tickerContainerW')

    if (cachedWidth && cachedWidth > 0) {
      console.log('✅ 读取本地缓存宽度:', cachedWidth)
      return
    }

    const query = uni.createSelectorQuery().in(instance.value.proxy)
    if (!instance.value) {
      console.warn('getCurrentInstance 为 null')
      return
    }

    query.selectAll('#tickerContainer').boundingClientRect(rect => {
      console.log(' = noteName tickerContainer', rect)

      if(rect && rect[0] && rect[0].width > 0){
        uni.setStorageSync('tickerContainerW', rect[0].width)
      }

    }).exec()


    // 各个数值
    props.argList.forEach((els, index) => {

      const query1 = uni.createSelectorQuery().in(instance.value.proxy)
      query1.select('#ticker' + props.noteName + index).boundingClientRect(rect => {
        console.log("++++++++++++++++++++++")
        console.log(' = props.noteName =', props.noteName)
        console.log('rect.width : tickerContainerW',rect.width,' : ',uni.getStorageSync('tickerContainerW'))
        console.log("++++++++++++++++++++++")
        if (rect) {
          if (rect.width >  uni.getStorageSync('tickerContainerW')) {
            tickerArr.value[index] = true
          } else {
            tickerArr.value[index] = false
          }
        }

      }).exec();
    })

  })

  onBeforeUnmount(() => clearInterval(timer))
})

// 监听 argList 变化
watch(
  () => props.argList,
  (newVal) => {
    newVal.forEach((item, index) => {
      if (item.prevValue && item.value !== item.prevValue) {
        startAnimation(index, item.value, item.prevValue)
      } else {
        displayValues.value[index] = {
          current: item.value,
          prev: item.prevValue ? item.value : item.prevValue,
          showPrev: false,
        }
      }
    })
    // tickerChange()
  },
  { deep: true },
)

const tickerChange = () => {

  //   let tickerContainerW = 0
  //
  //   const instance = getCurrentInstance()
  //   const query = uni.createSelectorQuery().in(instance.proxy)
  //   if (!instance) {
  //     console.warn('getCurrentInstance 为 null')
  //     return
  //   }
  //
  //   query.selectAll('#tickerContainer').boundingClientRect(rect => {
  //     console.log(' = noteName tickerContainer', rect)
  //     if (rect && tickerContainerW === 0) {
  //       tickerContainerW = rect[0].width
  //       console.log("= tickerContainerW width",tickerContainerW)
  //     }
  //   }).exec();
  //
  // console.log("= 固定宽 tickerContainerW width",tickerContainerW)
  //
  //   props.argList.forEach((els, index) => {
  //     const instance = getCurrentInstance()
  //     const query = uni.createSelectorQuery().in(instance.proxy)
  //     query.select('#ticker' + props.noteName + index).boundingClientRect(rect => {
  //       console.log("++++++++++++++++++++++")
  //       console.log(' = props.noteName =', props.noteName)
  //       console.log('rect.width : tickerContainerW',rect.width,' : ',tickerContainerW)
  //       console.log("++++++++++++++++++++++")
  //       if (rect) {
  //         if (rect.width > tickerContainerW) {
  //           tickerArr.value[index] = true
  //         } else {
  //           tickerArr.value[index] = false
  //         }
  //       }
  //
  //     }).exec();
  //   })
  //
  //   console.log('noteName tickerArr', tickerArr.value)

}

const tickerContainer = ref("tickerContainer")

// 开始动画
const startAnimation = (
  index: number,
  currentValue: string | number,
  prevValue: string | number,
) => {
  displayValues.value[index] = {
    current: currentValue,
    prev: prevValue,
    showPrev: true,
  }

  let count = 0
  const maxCount = 10 // 10秒 / 1秒 = 10次

  const interval = setInterval(() => {
    count++
    if (count >= maxCount) {
      clearInterval(interval)
      displayValues.value[index].showPrev = false
      return
    }

    displayValues.value[index].showPrev = !displayValues.value[index].showPrev
  }, 1000)
}

const toDetail = (item: { value: string | number; unit: string; name: string }) => {
  emit('toDeviceDetail', item)
}
const dynamicFormatTime = (timestamp: number) => {
  refreshCounter.value // 依赖计数器,强制重新计算
  return formatTime(timestamp) // 调用原始方法
}
</script>

<template>
  <wd-row custom-class="device-body" >
    <wd-col :span="12" custom-class="mt-4 arg-top" class="mt-4" v-for="(item, key) in props.argList" :key="key">
      <view
        class="arg-item mt-4"
        :class="{ _box: item.value !== item.prevValue }"
        @click="toDetail(item)"
      >
        <wd-row style="display: flex">
          <wd-col :span="19">
            <view class="arg-name">
              <p>{{ $te('element.' + item.yeuSign) ? $t('element.' + item.yeuSign) : $t('element.default') }}</p>
            </view>

            <view :id="tickerContainer" class="arg-value">
              <view :class="tickerArr[key]==true?'active-arg-value-text':''" :id="'ticker'+props.noteName+key" class="value-container">
                <view class="value-animation" :class="{
                    'prev-value': displayValues[key]?.showPrev,
                    'current-value': !displayValues[key]?.showPrev,
                  }">
                  <view>
                    {{
                      displayValues[key]?.showPrev
                        ? displayValues[key]?.prev
                        : displayValues[key]?.current
                    }}
                  </view>

                </view>
                <view class="value-unit">{{ item.unit || item.elementUnit || item.yeuUnit }}</view>
              </view>
            </view>

            <view class="arg-datetime">{{ dynamicFormatTime(item.time) }}</view>
          </wd-col>
          <wd-col
            :span="5"
            custom-class="arg-url"
          >
            <view v-if="item.yeuUrl" style="width: 100%; display: flex; align-items: center; justify-content: flex-end; height: 100%">
              <img
                style="width: 36rpx; height: 36rpx; object-fit: contain"
                :src="item.yeuUrl"
                alt=""
                srcset=""
              />
            </view>
            <view v-else>--</view>

          </wd-col>
        </wd-row>
      </view>
    </wd-col>
  </wd-row>
</template>

<style scoped lang="scss">
  :deep(.device-body) {
    display: flex!important;
    flex-wrap: wrap!important;
    justify-content: space-between!important;
  }

  ::v-deep {
    .device-body {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
    }
  }
  ::v-deep {
    .arg-top {
      margin-top: 6px!important;
    }
  }

  ::v-deep {
    .arg-url {
      display: flex!important;
      align-items: center!important;
      justify-content: flex-end!important;
      height: 100%!important;
    }
  }


  :deep(.wd-col__12.data-v-ce06229d) {
    margin-top: 6px!important;
  }

  .wd-row::after {
    content: none !important;
  }

  .device-body {
    display: flex;
    flex-wrap: wrap;
    gap: 10rpx;
    justify-content: space-between;
  }

  .arg-item {
    box-sizing: border-box;
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: center;
    width: 326rpx;
    min-height: 160rpx;
    padding: 15rpx;
    margin: 0 auto;
    background-color: #ffffff;
    border-radius: 20rpx;
  }

  :deep(.arg-datetime) {
    width: 280rpx;
    padding: 8rpx 0;
    font-size: 21rpx;
    font-weight: 300;
    color: #565656;
  }

  :deep(.arg-name) {
    font-size: 25rpx;
    font-weight: 400;
    color: #000000;
  }

  :deep(.arg-value) {
    font-size: 42rpx;
    font-weight: bold;
    font-family: monospace;
    color: #000000;
    position: relative;
    height: 50rpx;
    overflow: hidden;

    .value-container {
      position: absolute;
      display: flex;
      align-items: center;
    }

    .value-animation {
      transition: all 0.3s ease;
      white-space: nowrap;
    }

    .active-arg-value-text {
      animation: scroll-left 5s linear infinite;
    }

    @keyframes scroll-left {
      0% {
        left: 100%;
      }

      100% {
        left: -100%;
      }
    }

    .value-unit {
      margin-left: 4rpx;
      color: #000000;
      font-size: 26rpx;
      font-weight: normal;
    }

    .prev-value {
      color: red;
    }

    .current-value {
      color: inherit;
    }
  }

  :deep(.arg-icon) {
    width: 69rpx;
    height: 69rpx;
    background: rgba(154, 200, 251, 0.1);
    border-radius: 50%;
  }

  ._box {
    animation: breathe 1000ms ease-in-out 10 alternate;
  }

  @keyframes breathe {
    30% {
      box-shadow: 0rpx 1rpx 10rpx 1rpx #8eceac;
    }

    50% {
      box-shadow: 0rpx 1rpx 10rpx 1rpx #5ece96;
    }

    100% {
      box-shadow: 0rpx 1rpx 10rpx 1rpx #2dce89;
    }
  }
</style>