前端大屏常用的适配方案
假设我们正在开发一个可视化拖拽的搭建平台,可以拖拽生成工作台或可视化大屏,或者直接就是开发一个大屏,首先必须要考虑的一个问题就是页面如何适应屏幕,因为我们在搭建或开发时一般都会基于一个固定的宽高,但是实际的屏幕可能大小不一,接下来我们就尝试几种简单且常见的方案,并简单分析一下利弊。
demo
首先写个基础的demo
给后续使用:
<script setup>
import { ref } from "vue";
import Widget from "./components/Widget.vue";
import LineChart from "./components/LineChart.vue";
import BarChart from "./components/BarChart.vue";
import PieChart from "./components/PieChart.vue";
import FunnelChart from "./components/FunnelChart.vue";// 画布宽高
const canvasWidth = ref(1920);
const canvasHeight = ref(1080);
// 组件宽高
const widgetWidth = ref(960);
const widgetHeight = ref(540);
</script><template><div class="canvasBox"><divclass="canvas":style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"><Widget :width="widgetWidth" :height="widgetHeight" :left="0" :top="0"><LineChart></LineChart></Widget><Widget :width="widgetWidth" :height="widgetHeight" :left="widgetWidth" :top="0"><BarChart></BarChart></Widget><Widget :width="widgetWidth" :height="widgetHeight" :left="0" :top="widgetHeight"><PieChart></PieChart></Widget><Widget :width="widgetWidth" :height="widgetHeight" :left="widgetWidth" :top="widgetHeight"><FunnelChart></FunnelChart></Widget></div></div>
</template><style scoped>
.canvasBox {width: 100vw;height: 100vh;
}
.canvas {position: relative;
}
</style>
每个图表组件的宽高都设为
100%
,然后都被Widget
组件包裹,所以实际宽高是依赖Widget
组件的,Widget
组件为绝对定位,并且宽高、位置通过props
传入,模拟我们的拖拽操作,简单起见,所有图表的宽高我们都设为了相同的。
Widget
组件:
<script setup>
const props = defineProps({width: {type: Number,default: 0,},height: {type: Number,default: 0,},left: {type: Number,default: 0,},top: {type: Number,default: 0,},
});
</script><template><divclass="widgetBox":style="{width: width + 'px',height: height + 'px',left: left + 'px',top: top + 'px',}"><slot></slot></div>
</template><style scoped>
.widgetBox {position: absolute;
}
</style>
组件整体的容器为类名为
canvas
的元素,相对定位,宽高也是动态设置的,canvas
元素的父级canvasBox
元素宽高设为和屏幕宽高一致。
固定尺寸
即宽度、高度固定,如果宽高小于屏幕宽高则在屏幕居中。
这个是最简单的方案了,相当于不适配屏幕,画布配置了多大实际就是多大,不随屏幕的变化而变化,所以各个组件的宽高也是在配置后不会改变,一般用于尺寸固定且后期不会改变的可视化大屏。
我们前面的demo
初始就是这种方式:
当然,如果宽高小于屏幕的话居中的逻辑需要加一下,居中的方法有很多,通过css
、js
都可,根据自己的喜好来就行:
// 画布的位置
const canvasLeft = ref(0);
const canvasTop = ref(0);
// 如果屏幕的宽或高比画布的大,那么居中显示
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
if (windowWidth > canvasWidth.value) {canvasLeft.value = (windowWidth - canvasWidth.value) / 2;
}
if (windowHeight > canvasHeight.value) {canvasTop.value = (windowHeight - canvasHeight.value) / 2;
}<divclass="canvas":style="{width: canvasWidth + 'px',height: canvasHeight + 'px',left: canvasLeft + 'px',top: canvasTop + 'px',}">
</div>
判断窗口宽度和高度是否大于画布的宽高,是的话通过left
或top
来调整:
自适应宽度
即宽度适应屏幕,高度不变,这种方案的缺点是垂直方向上会出现滚动条。
比如画布设置的宽度为1920
,但是实际上屏幕的宽度为1280
,那么缩小了1.5
倍,那么画布和每个组件的宽度也需要同步缩小1.5
倍,并且每个组件的left
值也需要进行动态调整。
首先实现一下容器元素canvas
的尺寸调整:
// 保存原始画布的宽度
const originCanvasWidth = ref(canvasWidth.value);
// 宽度缩放比例
const ratioWidth = ref(1);// 当前窗口的宽度
let windowWidth = window.innerWidth;
// 将画布宽度设置为当前窗口的宽度
canvasWidth.value = windowWidth;
// 计算当前宽度和原始宽度的比例
ratioWidth.value = windowWidth / originCanvasWidth.value;
然后再把这个比例传给Widget
组件进行调整:
<Widget :ratioWidth="ratioWidth"><LineChart></LineChart>
</Widget>
<Widget :ratioWidth="ratioWidth"><BarChart></BarChart>
</Widget>
<Widget :ratioWidth="ratioWidth"><PieChart></PieChart>
</Widget>
<Widget :ratioWidth="ratioWidth"><FunnelChart></FunnelChart>
</Widget>
在Widget
组件里我们只要把宽度和left
都乘以这个比例即可,为什么是乘,很简单:
newWidth / width = ratioWidth = windowWidth / originCanvasWidth
newWidth = width * ratioWidth// left同样看做是一个距左侧的宽度即可<divclass="widgetBox":style="{width: width * ratioWidth + 'px',height: height + 'px',left: left * ratioWidth + 'px',top: top + 'px',}"><slot></slot>
</div>
自适应屏幕
即宽高都自适应,和上一种方案相比,这种横竖都不会出现滚动条,且能完全铺满屏幕。
实现也很简单,在上一个【自适应宽度】的基础上加上高度自适应即可。
// 画布原始宽高
const originCanvasWidth = ref(canvasWidth.value);
const originCanvasHeight = ref(canvasHeight.value);
// 缩放比例
const ratioWidth = ref(1);
const ratioHeight = ref(1);// 当前窗口的宽高
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
// 将画布宽高设置为当前窗口的宽高
canvasWidth.value = windowWidth;
canvasHeight.value = windowHeight;
// 计算当前宽高和原始宽高的比例
ratioWidth.value = windowWidth / originCanvasWidth.value;
ratioHeight.value = windowHeight / originCanvasHeight.value;
同样再把比例传给Widget
组件进行调整:
<Widget :ratioWidth="ratioWidth" :ratioHeight="ratioHeight"><LineChart></LineChart>
</Widget>
<Widget :ratioWidth="ratioWidth" :ratioHeight="ratioHeight"><BarChart></BarChart>
</Widget>
<Widget :ratioWidth="ratioWidth" :ratioHeight="ratioHeight"><PieChart></PieChart>
</Widget>
<Widget :ratioWidth="ratioWidth" :ratioHeight="ratioHeight"><FunnelChart></FunnelChart>
</Widget><divclass="widgetBox":style="{width: width * ratioWidth + 'px',height: height * ratioHeight + 'px',left: left * ratioWidth + 'px',top: top * ratioHeight + 'px',}"><slot></slot>
</div>
整体等比例缩放
即通过css
的transform
属性来对组件容器canvas
进行整体的缩放,保持原比例,在屏幕居中显示,当然你可以选择只缩放宽度或高度,但是这样会变形。
前面的两种方案,我们的组件开发时都必须要考虑容器的宽高,即需要进行适配,但是宽高比太极限了说实话很难处理,显示效果肯定是比较差的,但是这种整体等比例适配就无需考虑这种情况。
实际项目中如果有大屏需要适应屏幕,我一般都通过这种方法,优点是简单,缺点是水平或垂直空间上可能会留白,但是背景是全屏的,所以效果也不会很差。
实现也很简单,计算一下画布原始比例,再计算一下屏幕的比例,然后再判断是宽度和屏幕一致,高度自适应,还是高度和屏幕一致,宽度自适应:
// 当前窗口宽高比例
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let windowRatio = windowWidth / windowHeight;
// 画布原始宽高比例
const canvasRatio = canvasWidth.value / canvasHeight.value;
// 计算画布适应后的新宽高
let newCanvasWidth = 0;
let newCanvasHeight = 0;
if (canvasRatio > windowRatio) {// 画布的宽高比大于屏幕的宽高比// 画布的宽度调整为屏幕的宽度newCanvasWidth = windowWidth;// 画布的高度根据画布原比例进行缩放newCanvasHeight = windowWidth / canvasRatio;
} else {// 画布的宽高比小于屏幕的宽高比// 画布的高度调整为屏幕的高度newCanvasHeight = windowHeight;// 画布的宽度根据画布原比例进行缩放newCanvasWidth = windowHeight * canvasRatio;
}
// ...
假设屏幕的宽高相同,那么比例为1
。
第一种情况,假设画布的宽是高的两倍,那么比例为2
,要保持原比例2
适应屏幕,显然只能宽度和屏幕一致,高度自适应,因为如果高度和屏幕一致,那么宽度需要是高度的两倍,屏幕显然显示不下:
第二种情况,假设画布的高是宽的两倍,那么比例为0.5
,要保持比例为0.5
适应屏幕,需要高度和屏幕一致,宽度自适应:
计算完了画布适应屏幕后的新宽高,接下来就可以计算它相对于画布原始宽高的缩放比例:
// ...
// 相对于画布原始宽高的缩放比例
const canvasStyle = reactive({transform: "",
});
const scaleX = newCanvasWidth / canvasWidth.value;
const scaleY = newCanvasHeight / canvasHeight.value;
canvasStyle.transform = `scale(${scaleX}, ${scaleY})`
把样式添加到容器元素canvas
上即可:
<divclass="canvas":style="{width: canvasWidth + 'px',height: canvasHeight + 'px',...canvasStyle}">
</div>
显示的位置似乎有点问题,这其实是因为默认情况下元素的变换都是以自身的中心点为原点进行变换的:
我们只要改成以左上角为原点即可:
const canvasStyle = reactive({transform: "",transformOrigin: `left top`// 改成以左上角为变换原点
});
最后再来让它居中:
// 居中
const translateX = (windowWidth - newCanvasWidth) / 2 / scaleX;
const translateY = (windowHeight - newCanvasHeight) / 2 / scaleY;
canvasStyle.transform = `scale(${scaleX}, ${scaleY}) translate(${translateX}px, ${translateY}px)`;
窗口的宽高减去画布适应后的新宽高,即剩余的空间,再除以2
进行居中显示,为什么还要除以缩放值呢,因为translate
的值也会随scale
进行缩放,比如translateX
计算出来为100
,scaleX
为0.5
,那么实际上最终的偏移量为100*0.5=50
,这显然不对,所以我们除一个缩放值进行抵消。
这个方案似乎很完美,那么还有没有问题呢,显然是有的,一个小问题是缩放后文字可能会模糊,这个问题不大,笔者遇到的另一个问题是如果使用了getBoundingClientRect
方法获取元素信息,本意是获取元素原始的尺寸数据,但是缩放后返回的就是缩放后的数据,那么可能会和我们的原始意图出现偏差,比如有一个如下的div
:
<div ref="el1" style="width: 200px; height: 200px; background: red; position: absolute; left: 50px; top: 50px;"></div>
我们想要动态根据这个div
大小和位置复制一个div
:
<div ref="el2" style="background: green; position: absolute"></div>const { width, height } = el1.value.getBoundingClientRect();
const { left, top } = window.getComputedStyle(el1.value);
el2.value.style.width = `${width}px`;
el2.value.style.height = `${height}px`;
el2.value.style.left = left;
el2.value.style.top = top;
可以看到获取到的宽高比实际的小了一点,这显然不是我们需要的,解决方法是要么不要使用getBoundingClientRect
方法,使用offsetWdith
等不会被缩放影响的方法或属性获取元素尺寸,要么把获取到的数据除以缩放值。
当然可能还会存在其他一些属性或方法也会存在这个问题,这就需要各位在实际的开发时进行测试了。
总结
本文简单总结了一下大屏适配的几种方法,没有哪一种是最好的,也没有哪一种是非常完美的,没办法,很多时候都是需要进行一定妥协的。
demo
地址:Vite + Vue
demo
仓库地址:GitHub - skyWangZhenDong/palteform-fit-demo: 大屏的自适应
相关文章:

前端大屏常用的适配方案
假设我们正在开发一个可视化拖拽的搭建平台,可以拖拽生成工作台或可视化大屏,或者直接就是开发一个大屏,首先必须要考虑的一个问题就是页面如何适应屏幕,因为我们在搭建或开发时一般都会基于一个固定的宽高,但是实际的…...
技术债 笔记
目录 1. 技术债 笔记1.1. 什么是技术债1.2. 讨论1.3. 国内技术从业者怎么看? 1. 技术债 笔记 1.1. 什么是技术债 1992 年, Ward Cunningham 在敏捷宣言中首次提出了"技术债"概念, 主要指有意或无意地做了错误的或不理想的技术决策所累积的债务。随后, 《重构》一书…...

【Leetcode】102.二叉树的层序遍历
一、题目 1、题目描述 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。 示例1: 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]示例2: 输入:root = [1] 输出:[[1]]示例3: 输入:root = [] 输出:[]…...
上传文件报413Request EntityToo Large错误解决办法
产生这种原因是因为服务器限制了上传大小 1、nginx服务器的解决办法 修改nginx.conf的值就可以解决了 将以下代码粘贴到nginx.conf内 client_max_body_size 20M 可以选择在http{ }中设置:client_max_body_size 20m; 也可以选择在server{ }中设置:cli…...
Neo4j之MERGE基础
在 Neo4j 中,MERGE 语句用于根据指定的模式进行创建或匹配节点和关系。它可以在节点或关系不存在时创建它们,并在已存在时进行匹配。 创建或匹配节点: MERGE (p:Person {name: John});这个查询会检查是否已经存在一个具有 "Person&quo…...
AbstractRoutingDataSource,spring配置多数据源问题
AbstractRoutingDataSource,spring配置多数据源问题 首先引入pom.xml依赖 <!--测试--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.3.12.RE…...

日常BUG—— SpringBoot项目DEBUG模式启动慢、卡死。
😜作 者:是江迪呀✒️本文关键词:日常BUG、BUG、问题分析☀️每日 一言 :存在错误说明你在进步! 一、问题描述 我们调试程序时,需要使用DEBUG模式启动SpringBoot项目, 有时候会发…...

Linux网络编程(TCP状态转换关系)
文章目录 前言一、TCP状态转换图二、TCP连接状态转换解析三、TCP断开状态转换解析四、为什么需要有2MLS时长总结 前言 本篇文章来讲解一下TCP的状态转换关系,学习这个状态转换关系对于我们深入了解网络编程是非常有必要的。 一、TCP状态转换图 二、TCP连接状态转换…...

tauri-vue:快速开发跨平台软件的架子,支持自定义头部UI拖拽移动和窗口阴影效果
Tauri Vue Typescript 一个使用 taurivuets 开发跨平台软件的模板,支持窗口头部自定义 UI 和拖拽和窗口阴影,不用再自己做适配了,拿来即用,非常 nice。而且已经封装好了 tauri 的 http 请求工具,省去很多弯路。开源…...

做好以下几点,可以让我们延长周末体验感,好好放松!!!
工作以后常常容易感到疲于奔命,让我们找到适合自己方式,来让我们度过一个充实放松的周末! 方向一:分享你周末的时间规划 我们可以把每个月当做一个周期,制定一个简单的计划,如:第一周,锻炼身体…...

Python 学习笔记——代码基础
目录 Python基础知识 变量 赋值 数据类型 print用法 print格式化输出 运算符 if-else 数据结构 元组 in运算符 列表 切片 [ : ] 追加 append() 插入 insert() 删除 pop() 字典 循环 for循环 for循环应用——遍历 for循环应用——累加…...
Android Studio 无法正常导入项目
Android Studio 无法正常导入 model,运行按钮边出现“Add Configuration”,可进行以下方法处理: 解决办法: 1、点击Run三角按钮左边紧挨的下拉按钮,选择Edit Configuration,选择 Default 新建一个Android…...

Grafana+Prometheus技术文档-进阶使用-监控spring-boot项目
阿丹: 之前已经实现了使用Prometheus来对服务器进行了监控和仪表盘的创建,现在就需要对这些监控方法使用在spring-boot中去。 实现思路: 1、集成Actuator 2、加入Prometheus的依赖 3、配置开放端口、以及开放监控 4、配置Prometheus中的配置…...

PG常用SQL
数据库 创建数据库 PostgreSQL 创建数据库可以用以下三种方式: 1、使用 CREATE DATABASE SQL 语句来创建。2、使用 createdb 命令来创建。3、使用 pgAdmin 工具。 CREATE DATABASE 创建数据库 CREATE DATABASE 命令需要在 PostgreSQL 命令窗口来执行࿰…...

分模块开发的意义及开发步骤
🐌个人主页: 🐌 叶落闲庭 💨我的专栏:💨 c语言 数据结构 javaweb 石可破也,而不可夺坚;丹可磨也,而不可夺赤。 Maven进阶 一、分模块开发1.1分模块开发的意义1.2分模块开…...
vue-router中的一些 API
在Vue.js的vue-router中,一些重要api 1、RouterHistory:这是 vue-router 提供的路由历史记录对象。它可以跟踪当前页面的路由历史,并提供一些方法和属性来管理导航和历史记录。在 vue-router 中,有两种类型的路由历史记录对象&…...

go-zero 是如何实现令牌桶限流的?
原文链接: 上一篇文章介绍了 如何实现计数器限流?主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码。 但是采用固定窗口实现的限流器会有两个问题: 会出现请求量超出限…...
Oracle/PL/SQL奇技淫巧之ROWNUM伪列
ROWNUM伪列 ROWNUM是一个伪列,它是根据每次查询的结果动态生成的一列递增编号,表示 Oracle 从表中选择该行的顺序,选择的第一行ROWNUM为1,第二行ROWNUM为2,以此类推。 注意1: ROWNUM伪列是在WHERE子句之…...

“MongoDB基础知识【超详细】
"探索MongoDB的无边之境:沉浸式数据库之旅" 欢迎来到MongoDB的精彩世界!在这个博客中,我们将带您进入一个充满创新和无限潜力的数据库领域。无论您是开发者、数据工程师还是技术爱好者,MongoDB都将为您带来一场令人心动…...

腾讯24届校招内推
校招开始啦~有兴趣的话可以扫我的码投,也可以分享给身边找工作的同学~ ❤投递攻略 1️⃣腾讯校招步骤,先微信扫码绑定内推关系,后在电脑上上传更改简历和部门投递 2️⃣投递时将选择投递部门,投递后将在…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...