前端组件库—— Vue 大屏组件库知识点大全(三)

简介: 教程来源 https://www.amwtm.cn/ 数字翻牌器是大屏核心组件,以翻牌动画动态呈现关键指标(如GMV、用户数),支持千分位、小数位、单位配置及可视区懒启动,强化数据变化感知。

五、数据展示组件

5.1 数字翻牌器组件
数字翻牌器是大屏中最常见的数据展示组件之一,用于展示关键指标(如 GMV、用户数、订单量)。它的核心特点是:数据变化时,数字以“翻牌”的动画效果滚动变化,给用户强烈的数据变化感知。

设计思路:

将数字的每一位拆分成独立的位,分别进行动画

支持千分位分隔符、小数位数、单位等配置

支持进入可视区域才开始动画(懒启动)

<template>
  <div class="digital-flop" :class="{ 'is-visible': isVisible }" ref="flopRef">
    <div class="flop-title" v-if="title">{
  { title }}</div>
    <div class="flop-value-wrapper">
      <div class="flop-value">
        <!-- 前缀(如货币符号) -->
        <span v-if="prefix" class="flop-prefix">{
  { prefix }}</span>

        <!-- 整数部分(按位拆分) -->
        <span v-for="(digit, idx) in integerDigits" :key="idx" class="digit">
          <span class="digit-inner">{
  { digit }}</span>
        </span>

        <!-- 小数点 -->
        <span v-if="decimalPart !== null" class="decimal-point">.</span>

        <!-- 小数部分 -->
        <span v-for="(digit, idx) in decimalDigits" :key="idx" class="digit digit-decimal">
          <span class="digit-inner">{
  { digit }}</span>
        </span>

        <!-- 单位 -->
        <span v-if="unit" class="flop-unit">{
  { unit }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue'

const props = defineProps({
  value: { type: Number, required: true },
  title: { type: String, default: '' },
  unit: { type: String, default: '' },
  prefix: { type: String, default: '' },
  decimalPlaces: { type: Number, default: 0 },
  thousandSeparator: { type: Boolean, default: true },
  animationDuration: { type: Number, default: 1000 },
  useIntersectionObserver: { type: Boolean, default: true }
})

const isVisible = ref(!props.useIntersectionObserver)
const flopRef = ref(null)
const currentValue = ref(0)

// 格式化数字(添加千分位、小数位)
const formattedValue = computed(() => {
  const rounded = currentValue.value.toFixed(props.decimalPlaces)
  const parts = rounded.split('.')
  const integerPart = parts[0]
  const decimalPart = parts[1] || ''

  // 添加千分位分隔符
  const integerWithSeparator = props.thousandSeparator
    ? integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
    : integerPart

  return { integer: integerWithSeparator, decimal: decimalPart }
})

// 整数部分按位拆分为数组
const integerDigits = computed(() => {
  const intStr = formattedValue.value.integer
  return intStr.split('')
})

// 小数部分按位拆分
const decimalDigits = computed(() => {
  const decStr = formattedValue.value.decimal
  return decStr ? decStr.split('') : []
})

// 小数部分是否存在
const decimalPart = computed(() => props.decimalPlaces > 0)

// 数字滚动动画
const animateValue = () => {
  const startValue = currentValue.value
  const endValue = props.value
  const startTime = performance.now()

  const updateValue = (currentTime) => {
    const elapsed = currentTime - startTime
    const progress = Math.min(1, elapsed / props.animationDuration)

    // 使用 easeOutCubic 缓动函数,使动画更平滑
    const easeProgress = 1 - Math.pow(1 - progress, 3)
    currentValue.value = startValue + (endValue - startValue) * easeProgress

    if (progress < 1) {
      requestAnimationFrame(updateValue)
    } else {
      currentValue.value = endValue
    }
  }

  requestAnimationFrame(updateValue)
}

// 观察组件是否进入可视区域
let observer = null
if (props.useIntersectionObserver) {
  observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      isVisible.value = true
      observer?.disconnect()
      observer = null
    }
  }, { threshold: 0.1 })
}

onMounted(() => {
  if (observer && flopRef.value) {
    observer.observe(flopRef.value)
  }
  // 初始值直接设为目标值(避免动画)
  currentValue.value = props.value
})

// 监听 value 变化,触发动效
watch(() => props.value, () => {
  if (isVisible.value) {
    animateValue()
  } else {
    currentValue.value = props.value
  }
})
</script>

<style scoped>
.digital-flop {
  text-align: center;
  background: rgba(0, 0, 0, 0.4);
  border-radius: 12px;
  padding: 24px;
  backdrop-filter: blur(10px);
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.5s ease;
}

.digital-flop.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.flop-title {
  font-size: 18px;
  color: rgba(255, 255, 255, 0.7);
  margin-bottom: 16px;
  letter-spacing: 2px;
}

.flop-value {
  font-size: 56px;
  font-weight: bold;
  color: #00ffff;
  text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
  display: flex;
  justify-content: center;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px;
}

.digit {
  display: inline-block;
  min-width: 48px;
  text-align: center;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 8px;
  margin: 0 2px;
  padding: 8px 4px;
  position: relative;
  overflow: hidden;
  box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
}

.digit-inner {
  display: inline-block;
  animation: flipIn 0.3s ease;
}

@keyframes flipIn {
  0% { transform: translateY(-100%); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

.digit-decimal {
  min-width: 24px;
}

.decimal-point {
  font-size: 48px;
  margin: 0 2px;
  color: #00ffff;
}

.flop-prefix,
.flop-unit {
  font-size: 32px;
  margin: 0 8px;
  color: rgba(255, 255, 255, 0.7);
}

@media (max-width: 1024px) {
  .flop-value { font-size: 40px; }
  .digit { min-width: 36px; padding: 4px; }
  .decimal-point { font-size: 36px; }
}
</style>

六、实时数据与动态刷新

6.1 实时数据刷新策略
大屏应用通常需要展示实时数据,因此数据刷新是核心功能之一。常见的数据刷新策略有:
image.png
在大屏场景下,如果实时性要求高(如金融行情、网络监控),推荐使用 WebSocket;如果实时性要求一般(如每小时更新),使用轮询即可。

6.2 基于 WebSocket 的实时数据实现

// composables/useWebSocket.js
import { ref, onUnmounted } from 'vue'

export function useWebSocket(url, options = {}) {
  const {
    autoConnect = true,
    onMessage = () => {},
    onOpen = () => {},
    onClose = () => {},
    onError = () => {},
    reconnectInterval = 3000,
    maxReconnectAttempts = 10
  } = options

  const ws = ref(null)
  const isConnected = ref(false)
  const reconnectAttempts = ref(0)
  let reconnectTimer = null

  const connect = () => {
    ws.value = new WebSocket(url)

    ws.value.onopen = () => {
      isConnected.value = true
      reconnectAttempts.value = 0
      onOpen()
    }

    ws.value.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)
        onMessage(data)
      } catch (e) {
        console.error('WebSocket 数据解析失败:', e)
      }
    }

    ws.value.onclose = () => {
      isConnected.value = false
      onClose()

      // 自动重连
      if (reconnectAttempts.value < maxReconnectAttempts) {
        reconnectTimer = setTimeout(() => {
          reconnectAttempts.value++
          connect()
        }, reconnectInterval)
      }
    }

    ws.value.onerror = (error) => {
      onError(error)
    }
  }

  const send = (data) => {
    if (ws.value && isConnected.value) {
      ws.value.send(JSON.stringify(data))
    }
  }

  const close = () => {
    if (reconnectTimer) {
      clearTimeout(reconnectTimer)
    }
    if (ws.value) {
      ws.value.close()
    }
  }

  if (autoConnect) {
    connect()
  }

  onUnmounted(() => {
    close()
  })

  return { isConnected, send, close }
}

在组件中使用:

<script setup>
import { ref } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'

const realtimeData = ref({})

const handleMessage = (data) => {
  // 更新响应式数据
  realtimeData.value = data
  // 可以在这里触发动画或更新图表
}

const { isConnected } = useWebSocket('ws://localhost:8080/ws', {
  onMessage: handleMessage,
  onOpen: () => console.log('WebSocket 已连接'),
  onClose: () => console.log('WebSocket 已断开')
})
</script>

6.3 数据更新动画
为了让数据变化更加平滑,可以添加过渡动画:

<template>
  <div :key="dataVersion" class="data-container fade-in">
    <!-- 数据内容 -->
  </div>
</template>

<script setup>
import { ref } from 'vue'

const dataVersion = ref(0)

const updateData = (newData) => {
  data.value = newData
  // 刷新 key,触发重新渲染动画
  dataVersion.value++
}
</script>

<style>
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.fade-in {
  animation: fadeIn 0.3s ease;
}
</style>

七、性能优化

7.1 组件懒加载
对于大屏页面中的非首屏组件,可以使用 Vue 的异步组件进行懒加载:

import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() => 
  import('@/components/HeavyChart.vue')
)

7.2 图表实例复用与销毁

// 组件卸载时销毁图表实例,避免内存泄漏
onBeforeUnmount(() => {
  if (chartInstance) {
    chartInstance.dispose()
    chartInstance = null
  }
})

7.3 数据缓存
对于不常变化的数据(如配置项、字典数据),可以使用缓存减少请求:

const dataCache = new Map()

const fetchData = async (key, fetcher, ttl = 60000) => {
  const cached = dataCache.get(key)
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data
  }
  const data = await fetcher()
  dataCache.set(key, { data, timestamp: Date.now() })
  return data
}

7.4 虚拟滚动
对于超长列表(如日志、排行榜),使用虚拟滚动只渲染可视区域内的数据:

<template>
  <div class="virtual-list-container" ref="container" @scroll="handleScroll">
    <div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div>
    <div class="virtual-list-content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div v-for="item in visibleData" :key="item.id" class="list-item">
        {
  { item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  data: { type: Array, required: true },
  itemHeight: { type: Number, default: 50 },
  bufferSize: { type: Number, default: 5 }
})

const container = ref(null)
const scrollTop = ref(0)

const totalHeight = computed(() => props.data.length * props.itemHeight)

const startIndex = computed(() => {
  const index = Math.floor(scrollTop.value / props.itemHeight)
  return Math.max(0, index - props.bufferSize)
})

const endIndex = computed(() => {
  const visibleCount = Math.ceil(container.value?.clientHeight / props.itemHeight) || 0
  const index = startIndex.value + visibleCount + props.bufferSize
  return Math.min(props.data.length, index)
})

const visibleData = computed(() => props.data.slice(startIndex.value, endIndex.value))

const offsetY = computed(() => startIndex.value * props.itemHeight)

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
}
</script>

<style scoped>
.virtual-list-container {
  height: 400px;
  overflow-y: auto;
  position: relative;
}

.virtual-list-phantom {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
}

.virtual-list-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.list-item {
  height: 50px;
  padding: 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
</style>

来源:
https://amwtm.cn/

相关文章
|
3月前
|
消息中间件 存储 缓存
【Kafka核心】架构模型:Producer、Broker、Consumer、Consumer Group、Topic、Partition、Replica
本文系统解析Kafka 3.x+核心架构,涵盖Producer、Broker、Consumer、Group、Topic、Partition、Replica七大实体,深入KRaft新架构、ISR机制、零拷贝、幂等性、Exactly-Once等关键技术,构建从设计哲学到落地实践的完整知识闭环。
|
3月前
|
人工智能 小程序 程序员
零基础入门Vibe Coding的正确打开方式
本文是一位中文专业出身、零代码基础的文科生亲历Vibe Coding(氛围编程)的真实记录。三个月内,用AI工具自主开发出桌面整理、Excel图表生成、图片批量加水印等实用小工具。文章以通俗语言解析Vibe Coding本质——“说需求,AI写代码”,强调其门槛已从“会写代码”降至“会说话”,鼓励普通人放下畏惧,动手实践。
|
Linux 测试技术 网络安全
VoIP网络电话(一):服务器搭建
前段时间有朋友按照教程搭建服务器,登录时报错“Operation is unauthorized because missing credential”,最近一直没空出来时间解决一下。
2136 0
|
12天前
|
JSON 编解码 监控
淘宝拍立淘图片搜索API完整文档
本项目采用淘宝官方taobao.item_search_img图搜接口(拍立淘),v2.0版,MD5签名,JSON返回。支持JPG/PNG图片(≤5MB,推荐≤500KB),可传Base64或公网URL。适用于同款比价、竞品监控与铺货采集,白底纯色图识别率提升40%+。(239字)
|
21天前
|
人工智能 安全 数据安全/隐私保护
国际刑警报告视域下新加坡跨境网络犯罪多层协同防控体系研究
本文基于国际刑警组织东南亚反诈报告,聚焦新加坡治理困境,揭示产业化、AI伪造、资金洗白与执法协同四大难题;创新构建“立法—平台—AI检测—跨境溯源”四层闭环框架,研发三段可落地Python检测代码,将诈骗识别精度从39.6%提升至92.3%,为全球跨境网络犯罪协同治理提供理论支撑与工程化方案。(239字)
133 2
|
14天前
|
数据采集 NoSQL 调度
日本电商多平台数据聚合系统:Scrapy + Redis分布式架构设计
本文介绍基于Scrapy+Redis的分布式爬虫系统,专为日本代购、一站式日淘(含雅虎代拍、北极星日淘)设计,支持雅虎拍卖、煤炉、乐天等多平台数据聚合。架构含Master调度与多Worker节点,集成Redis去重、MongoDB/Elasticsearch存储,日均稳定采集50万+条,可用率99.5%。(239字)
75 0
|
3月前
|
JavaScript 前端开发 测试技术
前端开发环境搭建:Node.js、npm与VSCode指南
在当今快速发展的前端开发领域,一个高效、稳定的开发环境是提升生产力的关键。Node.js、npm和VSCode作为现代前端开发的三大核心工具,能够帮助开发者轻松管理依赖、运行脚本以及编写高质量代码。本文将介绍如何搭建这一开发环境,并深入探讨几个关键方面,助你快速上手。
|
10月前
|
人工智能 前端开发 Docker
从本地到云端:用 Docker Compose 与 Offload 构建可扩展 AI 智能体
在 AI 智能体开发中,开发者常面临本地调试与云端部署的矛盾。本文介绍如何通过 Docker Compose 与 Docker Offload 解决这一难题,实现从本地快速迭代到云端高效扩容的全流程。内容涵盖多服务协同、容器化配置、GPU 支持及实战案例,助你构建高效、一致的 AI 智能体开发环境。
893 2
从本地到云端:用 Docker Compose 与 Offload 构建可扩展 AI 智能体
|
存储 数据采集 机器学习/深度学习
新闻聚合项目:多源异构数据的采集与存储架构
本文探讨了新闻聚合项目中数据采集的技术挑战与解决方案,指出单纯依赖抓取技术存在局限性。通过代理IP、Cookie和User-Agent的精细设置,可有效提高采集策略;但多源异构数据的清洗与存储同样关键,需结合智能化算法处理语义差异。正反方围绕技术手段的有效性和局限性展开讨论,最终强调综合运用代理技术与智能数据处理的重要性。未来,随着机器学习和自然语言处理的发展,新闻聚合将实现更高效的热点捕捉与信息传播。附带的代码示例展示了如何从多个中文新闻网站抓取数据并统计热点关键词。
753 2
新闻聚合项目:多源异构数据的采集与存储架构
|
前端开发 JavaScript API
Howler.js:音频处理的轻量级解决方案
Howler.js:音频处理的轻量级解决方案
2288 0