由于我最近一直开发大屏,所以累计了一些经验,在此分享给大家,也方便后续自己复制粘贴...
内容主要是大屏一些可复用的模块,项目技术栈: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>
滚动只显示一条
实现代码如下: 这种方案纯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-…