快速搞定可视化大屏

8,191 阅读3分钟

由于我最近一直开发大屏,所以累计了一些经验,在此分享给大家,也方便后续自己复制粘贴...

内容主要是大屏一些可复用的模块,项目技术栈:vite + vue3 + echarts + vue-echarts

关于适配

vw、vh方案

此方案是利用插件postcss-px-to-viewport 将px单位转换成vw、vh相对单位,在开发时可以对标设计稿写px,插件会帮你自动转,大致实现代码如下: 首先安装插件

pnpm i postcss-px-to-viewport

然后在根目录下新建postcss.config.js

import createPlugin from 'postcss-px-to-viewport'

export default {
  plugins: [
    createPlugin({
      // Options...
      unitToConvert: 'px',
      viewportWidth: 1920,
      unitPrecision: 2,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 2,
      mediaQuery: false,
      replace: true,
      exclude: undefined,
      include: undefined,
      landscape: false,
      landscapeUnit: 'vh',
      landscapeWidth: 568,
    }),
  ],
}

此方案对比下面的方案优势在于可监听窗口大小去适配,不用手动刷新

根节点css缩放方案

此方案就是使用css transform: scale(${rate})属性去缩放根节点,代码如下:

const scale = () => {
  let styleWidth = 1920 // 大屏分辨率宽(设计稿与大屏分辨率一致)
  let wWidth = document.documentElement.clientWidth //可视窗口宽度
  let percentS = wWidth / styleWidth //缩放比例(原始高度/可视窗口高度)
  rate.value = percentS
  appStyles.value = {
    "transform-origin": "0% 0%",
    transform: `scale(${rate.value})`
  }
}
<div class="dashboard" :style="appStyles"></div>

时间组件

时间是大屏项目必不可少的组件,新建一个CurrentTime.vue的时间组件。这里需要特别说明一下,当页面失活也就是切换到其他页面后,定时器会变慢倒置切换回来后时间不准确,所以切换回来后需要重启一下定时器,当然真正放在大屏上展示的时候可能不存在切换页面的操作,就可以忽略这一步。

<template>
  <div class="current-date-time">
    <div class="current-date">{{ currentDate }}</div>
    <div class="current-time">{{ currentTime }}</div>
  </div>
</template>

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

const currentDate = ref('')
const currentTime = ref('')
let intervalId = null

onMounted(() => {
  // 获取当前日期和时间
  const updateDateTime = () => {
    const now = new Date()
    const year = now.getFullYear()
    const month = (now.getMonth() + 1).toString().padStart(2, '0')
    const day = now.getDate().toString().padStart(2, '0')
    const hours = now.getHours().toString().padStart(2, '0')
    const minutes = now.getMinutes().toString().padStart(2, '0')
    const seconds = now.getSeconds().toString().padStart(2, '0')
    currentDate.value = `${year}-${month}-${day}`
    currentTime.value = `${hours}:${minutes}:${seconds}`
  }

  updateDateTime()

  // 每秒更新一次当前日期和时间
  intervalId = setInterval(() => {
    updateDateTime()
  }, 1000)

  // 页面失活后定时器会变慢,时间会不准确,所以切换回来后重启定时
  window.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      clearInterval(intervalId)
      updateDateTime()
    }
  })
})

// 在组件卸载时清除计时器
onUnmounted(() => {
  clearInterval(intervalId)
})
</script>
<style>
.current-date-time {
  display: flex;
  align-items: center;
  color: rgba(255, 255, 255, 0.85);
  padding-left: 32px;
  font-size: 20px;
}
.current-time {
  margin-left: 12px;
}
</style>

天气组件

新建一个天气组件Weather.vue,这里的数据来源于api.openweathermap.org这个站点,大家可以去注册然后获取key

<template>
  <div class="weather">
    <img :src="iconUrl" alt="Weather icon" />
    {{ Math.round(temperature) }}°C
  </div>
</template>

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

const API_KEY = '3309dbe80bc5799fef421021735c8724'
const CITY = 'Beijing'

const city = ref('')
const temperature = ref('')
const weather = ref('')
const iconCode = ref('')
const iconUrl = ref('')

onMounted(async () => {
  try {
    const response = await axios.get(
      `https://api.openweathermap.org/data/2.5/weather?q=${CITY}&appid=${API_KEY}&units=metric&lang=zh_cn`
    )
    city.value = response.data.name
    temperature.value = response.data.main.temp
    weather.value = response.data.weather[0].description
    iconCode.value = response.data.weather[0].icon
    iconUrl.value = `http://openweathermap.org/img/w/${iconCode.value}.png`
  } catch (error) {
    console.error(error)
  }
})
</script>
<style lang="scss">
.weather {
  display: flex;
  align-items: center;
  color: rgba(255, 255, 255, 0.85);
  font-size: 20px;
  img {
    margin-right: 8px;
  }
}
</style>

全屏组件

<script setup>
import { ref } from 'vue'
const fullScreenState = ref(false)
const handleFullScreen = () => {
  if (!fullScreenState.value) {
    document.body.requestFullscreen()
  } else {
    document.exitFullscreen()
  }
  fullScreenState.value = !fullScreenState.value
}
</script>
<template>
  <span class="full-screen-btn" @click="handleFullScreen">
      <img
        v-show="!fullScreenState"
        src="@/assets/icon-fullscreen.svg"
        alt=""
      />
      <img
        v-show="fullScreenState"
        src="@/assets/icon-exit-fullscreen.svg"
        alt=""
      />
    </span>
</template>

滚动列表

分页滚动

实现代码如下: 这里使用了ant-design的carousel组件,arrivalRankData是一个二维数组

<template>
<div class="arrival-goods-rank">
  <ul class="arrival-goods-rank-header">
    <li>
      <span style="width: 13%">排名</span>
      <span style="width: 47%">发货单位</span>
      <span style="width: 20%">货物品类</span>
      <span style="width: 20%">累计车数</span>
    </li>
  </ul>
  <a-carousel
    v-if="arrivalRankData.length > 0"
    dot-position="right"
    :dots="false"
    autoplay
  >
    <ul
      :class="{ 'with-body': index === 0 }"
      class="arrival-goods-rank-body"
      v-for="(item, index) in arrivalRankData"
      :key="`rank${index}`"
    >
      <li
        v-for="(rankItem, rankIdx) in item"
        :key="`rankItem${rankIdx}`"
      >
        <span style="width: 13%">{{
          index === 0 ? "" : rankIdx + 1 + index * 6
        }}</span>
        <span style="width: 47%" :title="rankItem.fhrmc">{{
          rankItem.fhrmc
        }}</span>
        <span style="width: 20%">{{ rankItem.pmhz }}</span>
        <span style="width: 20%">{{ rankItem.cs }}</span>
      </li>
    </ul>
  </a-carousel>
</div>
</template>
<style lang="scss" scoped>
.arrival-goods-rank {
  width: 100%;
  height: 238px;
  background: url("../../assets/table-bg.png") no-repeat;
  background-size: cover;
  ul {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  li {
    display: flex;
    align-items: center;
    height: 34px;
    font-size: 12px;
    padding: 0 10px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
    color: #fff;
    span {
      flex-shrink: 0;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
  &-body {
    height: 204px;
    &.with-body {
      background: url("../../assets/ranking.png") no-repeat;
      background-size: 28px auto;
      background-position: 20px 12px;
    }
  }
}
</style>

一条条滚动

代码如下: 这里使用了transition-group,触发条件是定时往数据里新增一条数据

<script setup lang="ts">
    let cargoList = ref<any>([])
    let cargoAddList = ref<any>([])
    let listTimer: any = null

    const getBroadcastData = async () => {
      cargoList.value = []
      cargoAddList.value = []
      clearInterval(listTimer)
      listTimer = null
      const resp = await getBroadcast()
      if (resp.code === 0) {
        resp.data.map((item: any, index: number) => (item.No = index + 1))
        cargoList.value = resp.data.slice(0, 6)
        cargoAddList.value = resp.data.slice(6)
        cargoListScroll()
      }
    }

    const cargoListScroll = () => {
      listTimer = null
      if (!cargoAddList.value.length) return
      if (listTimer) return
      listTimer = setInterval(() => {
        const firstChild = cargoList.value.splice(0, 1)
        cargoAddList.value.push(...firstChild)
        const addFirstChild = cargoAddList.value.splice(0, 1)
        cargoList.value.push(...addFirstChild)
      }, 4000)
    }
</script>
<template>
    <section class="cargo-list">
        <h4 class="section-title">停限装播报</h4>
        <div class="cargo-list-container">
            <ul class="cargo-list-header">
                <li>
                  <span style="width: 10%">序号</span>
                  <span style="width: 30%">标题</span>
                  <span style="width: 20%">消息类型</span>
                  <span style="width: 20%">发布单位</span>
                  <span style="width: 20%">发布时间</span>
                </li>
            </ul>
            <div
            class="cargo-list-body"
            style="overflow: hidden; height: 220px"
            >
                <transition-group
                  v-if="cargoList.length > 0"
                  name="list-complete"
                  tag="ul"
                >
                  <li
                    v-for="item in cargoList"
                    :key="item.id"
                    class="list-complete-item"
                  >
                    <span style="width: 10%">{{ item.No }}</span>
                    <span style="width: 30%" :title="item.title">
                      {{ item.title }}
                    </span>
                    <span style="width: 20%">
                      <span
                        v-if="item.status === '停限装公告'"
                        class="status-tag status-tag--success"
                        >{{ item.messTypeHz }}</span
                      >
                      <span v-else class="status-tag status-tag--warning">{{
                        item.messTypeHz
                      }}</span>
                    </span>
                    <span style="width: 20%">{{ item.ljdm }}</span>
                    <span style="width: 20%">{{ item.publishTime }}</span>
                  </li>
                </transition-group>
            </div>
        </div>
    </section>
</template>
<style lang="scss">
.cargo-list {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  li {
    display: flex;
    align-items: center;
    width: 100%;
    height: 100%;
  }
  span {
    flex-shrink: 0;
    color: #fff;
    font-size: 12px;
    padding: 0 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
  }
  &-header {
    height: 34px;
    background: rgba(0, 137, 214, 0.15);
    mix-blend-mode: normal;
    position: relative;
    &::after {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 1px;
      display: block;
      content: "";
      background: linear-gradient(
        -90deg,
        rgba(0, 148, 255, 0.0001) 0%,
        rgba(31, 161, 255, 0.8257) 7.89%,
        rgba(38, 138, 255, 1) 48.23%,
        rgba(0, 255, 250, 1) 91.76%,
        rgba(0, 255, 250, 0.0001) 100%
      );
    }
  }
  &-body {
    position: relative;
    height: 250px;
    li {
      height: 44px;
    }
  }
}
.list-complete-item {
  transition: transform 1s;
}
.list-complete-enter,.list-complete-leave-active
/* .list-complete-leave-active for below version 2.1.8 */ {
  // opacity: 0;
  transform: translateY(-44px);
}
.list-complete-leave-active {
  position: absolute;
  table-layout: fixed;
}
</style>

滚动只显示一条

QQ录屏20230525120105.gif

实现代码如下: 这种方案纯css实现,在css中使用到了js变量,整个动画使用了2个keyframes, scroll用于切换滚动项,scroll2用于滚动项从底部过渡到顶部效果。

<template>
 <div class="num-card-item-list">
  <ul class="num-card-item-list-scroll">
    <li
      v-for="(item, index) in rxqIndicator.yxqGoodsIndicator"
      class="num-card-item"
      :key="`品类${index}`"
    >
      <span>{{ item.pmhz }}</span>
      <b>{{ item.count }}</b>
    </li>
    <li
      v-if="yxqGoodsIndicatorLength > 1"
      class="num-card-item">
      <span>{{ rxqIndicator.yxqGoodsIndicator[0].pmhz }}</span>
      <b>{{ rxqIndicator.yxqGoodsIndicator[0].count }}</b>
    </li>
  </ul>
</div>
</template>
<style lang="scss">
 .num-card-item-list {
    height: 50px;
    overflow: hidden;
    flex: 1;
    flex-shrink: 0;
    ul {
      margin: 0;
      padding: 0;
    }
    &-scroll {
      animation-name: scroll;
      animation-duration: v-bind(yxqGoodsIndicatorAniTime);
      animation-timing-function: steps(v-bind(yxqGoodsIndicatorLength));
      animation-iteration-count: infinite;
      li {
        animation-name: scroll2;
        animation-duration: 3s;
        animation-timing-function: linear;
        animation-iteration-count: infinite;
      }
    }
  }
  @keyframes scroll {
    0% {
      transform: translateY(0);
    }
    100% {
      transform: translateY(calc(-100% + 51px));
    }
  }
  @keyframes scroll2 {
    0% {
      transform: translateY(0);
    }
    60% {
      transform: translateY(0);
    }
    100% {
      transform: translateY(-100%);
    }
  }
</style>

图表

图表是大屏的核心,网上也有很多案例资源,给大家分享一篇为读者精心准备目前网络上最全面、最高效echarts案例资源站集合 juejin.cn/post/707883…

如果是vue项目也可以使用vue-echarts,具体使用可查看github
github.com/ecomfe/vue-…