大屏适配终极指南:6 种主流方案实战对比
数据可视化大屏开发中,屏幕适配是最令人头疼的问题之一。本文通过 6 个完整 Demo,深度对比 Scale、VW/VH、Rem、流式布局、响应式断点、混合方案这 6 种主流方案的优缺点与适用场景。
前言
在开发数据可视化大屏时,你是否遇到过这些问题:
- 设计稿是 1920×1080,实际大屏是 3840×2160,怎么适配?
- 大屏比例不是 16:9,出现黑边或拉伸怎么办?
- 小屏幕上字体太小看不清,大屏幕上内容太空旷
- 图表、地图、视频等组件在不同尺寸下表现不一致
本文将通过 6 个完整的可交互 Demo,带你逐一攻克这些难题。
方案一:Scale 等比例缩放
核心原理
通过 CSS transform: scale() 将整个页面按屏幕比例缩放,是最简单直观的方案。
javascript
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
function setScale() {
const scaleX = window.innerWidth / DESIGN_WIDTH;
const scaleY = window.innerHeight / DESIGN_HEIGHT;
const scale = Math.min(scaleX, scaleY);
screen.style.transform = `scale(${scale})`;
// 居中显示
const left = (window.innerWidth - DESIGN_WIDTH * scale) / 2;
const top = (window.innerHeight - DESIGN_HEIGHT * scale) / 2;
screen.style.left = left + 'px';
screen.style.top = top + 'px';
}
优缺点分析
优点:
- 完美还原设计稿比例
- 实现简单,几行代码搞定
- 字体、图表自动缩放,无需额外处理
- 兼容性好,不需要关注浏览器兼容性
缺点:
- 屏幕比例不符时出现黑边(如 4:3 屏幕)
- 小屏幕上字体会缩得很小,可能看不清
- 无法利用多余空间,浪费屏幕资源
适用场景
数据可视化大屏、监控中心大屏等需要精确还原设计稿的场景。
方案二:VW/VH 视口单位
核心原理
使用 CSS viewport 单位(vw/vh)代替 px,让元素根据视口尺寸自适应。
css
.container {
width: 50vw; /* 视口宽度的 50% */
height: 30vh; /* 视口高度的 30% */
font-size: 2vw; /* 字体随视口宽度变化 */
padding: 2vh 3vw; /* 内边距也自适应 */
}
优缺点分析
优点:
- 纯 CSS 方案,无 JS 依赖
- 充分利用全部屏幕空间,无黑边
- 无缩放导致的模糊问题
缺点:
- 计算公式复杂,需要手动换算
- 宽高难以协调,容易出现变形
- 单位换算易出错,维护成本高
适用场景
内容型页面、需要充分利用屏幕空间的场景。
方案三:Rem 动态计算
核心原理
根据视口宽度动态计算根字体大小,所有尺寸使用 rem 单位。
javascript
function setRem() {
const designWidth = 1920;
const rem = (window.innerWidth / designWidth) * 100;
document.documentElement.style.fontSize = rem + 'px';
}
// CSS 中使用 rem
.card {
width: 4rem; /* 设计稿 400px */
height: 2rem; /* 设计稿 200px */
font-size: 0.24rem; /* 设计稿 24px */
}
优缺点分析
优点:
- 计算相对直观(设计稿 px / 100 = rem)
- 兼容性最好,支持 IE9+
- 无缩放模糊问题
缺点:
- 依赖 JS 初始化,页面可能闪烁
- 高度处理困难,只能基于宽度适配
- 需要手动或使用工具转换单位
适用场景
移动端 H5 页面、PC 端后台系统。
方案四:流式/弹性布局
核心原理
使用百分比、Flexbox、Grid 等 CSS 原生能力实现响应式布局。
css
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.card {
display: flex;
flex-direction: column;
padding: 2%;
}
.chart {
flex: 1;
min-height: 200px;
}
优缺点分析
优点:
- 纯 CSS 方案,无任何依赖
- 自然响应式,适配各种屏幕
- 内容自适应,信息密度可变
缺点:
- 设计稿还原度低,无法精确控制
- 图表比例容易失控
- 空间利用率低,不够美观
适用场景
B 端后台系统、管理平台等以内容为主的页面。
方案五:响应式断点
核心原理
使用 @media 查询针对不同屏幕尺寸写多套样式规则。
css
/* 1920×1080 大屏 */
@media (min-width: 1920px) {
.container { width: 1800px; font-size: 24px; }
.grid { grid-template-columns: repeat(4, 1fr); }
}
/* 3840×2160 4K 屏 */
@media (min-width: 3840px) {
.container { width: 3600px; font-size: 48px; }
.grid { grid-template-columns: repeat(6, 1fr); }
}
/* 1366×768 小屏 */
@media (max-width: 1366px) {
.container { width: 1300px; font-size: 18px; }
.grid { grid-template-columns: repeat(3, 1fr); }
}
优缺点分析
优点:
- 精确控制各个分辨率的显示效果
- 字体可读性可控
- 适合有固定规格的大屏群
缺点:
- 代码量爆炸,维护极其困难
- 无法覆盖所有可能的分辨率
- 新增规格需要大量修改
适用场景
有固定规格的大屏群、展厅多屏展示系统。
方案六:混合方案(推荐)
核心原理
结合 Scale + Rem 的优点,既保证设计稿比例,又确保字体在小屏可读。
javascript
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
const MIN_SCALE = 0.6; // 最小缩放限制
function adapt() {
const winW = window.innerWidth;
const winH = window.innerHeight;
// Scale 计算 - 保证整体比例
const scaleX = winW / DESIGN_WIDTH;
const scaleY = winH / DESIGN_HEIGHT;
const scale = Math.max(Math.min(scaleX, scaleY), MIN_SCALE);
// 应用 scale
screen.style.transform = `scale(${scale})`;
// Rem 计算 - 根据缩放比例调整根字体
// 当 scale < 1 时,增加根字体补偿
const baseRem = 100;
const fontScale = Math.max(scale, MIN_SCALE);
const rem = baseRem * fontScale;
document.documentElement.style.fontSize = rem + 'px';
}
优缺点分析
优点:
- 等比例缩放保证布局一致性
- 字体最小值保护,防止过小不可读
- 大屏清晰、小屏可读
- 兼顾视觉和体验
缺点:
- 需要 JS 支持
- 计算逻辑稍复杂
适用场景
通用推荐方案,适合绝大多数大屏开发场景。
方案对比一览表
| 方案 | 实现难度 | 设计稿还原度 | 响应式表现 | 小屏可读性 | 维护成本 | 推荐场景 |
|---|---|---|---|---|---|---|
| Scale | 简单 | 极高 | 一般 | 差 | 低 | 数据可视化大屏 |
| VW/VH | 中等 | 中等 | 好 | 中等 | 中等 | 内容型页面 |
| Rem | 中等 | 高 | 一般 | 中等 | 中等 | 移动端 H5 |
| 流式布局 | 简单 | 低 | 极好 | 好 | 低 | B 端后台系统 |
| 断点方案 | 复杂 | 中-高 | 好 | 好 | 极高 | 固定规格大屏群 |
| 混合方案 | 中等 | 高 | 好 | 好 | 中等 | 通用推荐 |
场景推荐速查
🖥️ 数据可视化大屏 / 监控中心
需要精确还原设计稿,图表比例严格保持,像素级对齐。
推荐: Scale 等比例缩放(接受黑边)或 混合方案
示例: 企业展厅大屏、运营监控看板
📊 B 端后台 / 管理系统
内容为主,需要充分利用屏幕空间,信息密度要高。
推荐: 流式布局 或 VW/VH 方案
示例: CRM 系统、数据管理平台
🌐 多端适配 / 响应式网站
需要覆盖手机、平板、电脑、大屏等多种设备。
推荐: 响应式断点 + 流式布局
示例: 企业官网、数据门户
完整 Demo 代码
以下是 6 种大屏适配方案的完整可运行代码,保存为 HTML 文件后可直接在浏览器中打开体验。
Demo 1: Scale 等比例缩放
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案1: Scale 等比例缩放 - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
/* 信息面板 - 不参与缩放 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 20px;
border-radius: 8px;
z-index: 9999;
width: 320px;
font-size: 14px;
}
.info-panel h3 {
color: #4fc3f7;
margin-bottom: 12px;
font-size: 16px;
}
.info-panel .pros-cons {
margin-bottom: 15px;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #e57373;
}
.info-panel .current-scale {
background: #ff9800;
color: #000;
padding: 8px 12px;
border-radius: 4px;
font-weight: bold;
margin-top: 10px;
}
/* 大屏容器 - 按 1920*1080 设计 */
.screen-container {
width: 1920px;
height: 1080px;
transform-origin: 0 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
position: relative;
overflow: hidden;
}
/* 大屏内容样式 */
.header {
height: 100px;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #fff;
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
letter-spacing: 8px;
}
.main-content {
display: flex;
padding: 30px;
gap: 20px;
height: calc(100% - 100px);
}
.sidebar {
width: 400px;
background: rgba(15, 52, 96, 0.3);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.chart-area {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(79, 195, 247, 0.2);
display: flex;
flex-direction: column;
}
.card-title {
color: #4fc3f7;
font-size: 24px;
margin-bottom: 15px;
}
.card-value {
color: #fff;
font-size: 56px;
font-weight: bold;
}
.mini-chart {
flex: 1;
margin-top: 15px;
border-radius: 8px;
min-height: 180px;
}
.notice {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
background: rgba(255, 152, 0, 0.2);
border: 1px solid #ff9800;
color: #ff9800;
padding: 15px 20px;
border-radius: 8px;
font-size: 18px;
}
.grid-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
border-top: 2px dashed rgba(79, 195, 247, 0.1);
}
.grid-line::before {
content: '设计稿中心线 (960px)';
position: absolute;
left: 50%;
transform: translateX(-50%);
color: rgba(79, 195, 247, 0.3);
font-size: 14px;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<h3>方案1: Scale 等比例缩放</h3>
<div class="pros-cons">
<div class="pros">✓ 优点:</div>
• 完美还原设计稿比例<br>
• 实现简单直观<br>
• 字体/图表自动缩放<br><br>
<div class="cons">✗ 缺点:</div>
• 屏幕比例不符时出现黑边<br>
• 字体过小可能看不清<br>
• 无法利用多余空间
</div>
<div class="current-scale" id="scaleInfo">
缩放比例: 1.0<br>
窗口尺寸: 1920×1080
</div>
</div>
<!-- 大屏容器 -->
<div class="screen-container" id="screen">
<div class="header">SCALE 方案演示 - 1920×1080 设计稿</div>
<div class="main-content">
<div class="sidebar">
<div class="card-title">左侧信息面板</div>
<p style="color: rgba(255,255,255,0.7); font-size: 18px; line-height: 1.8;">
这是基于 1920×1080 设计稿开发的页面。<br><br>
修改浏览器窗口大小,观察整个页面如何等比例缩放。注意两侧的空白区域(当屏幕比例不是 16:9 时)。
</p>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value" id="value1">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value" id="value2">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value" id="value3">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value" id="value4">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
<div class="grid-line"></div>
<div class="notice">
💡 提示:调整浏览器窗口为 4:3 比例或手机尺寸,观察两侧的黑边/留白。这是 Scale 方案的典型特征。
</div>
</div>
<script>
const screen = document.getElementById('screen');
const scaleInfo = document.getElementById('scaleInfo');
// 设计稿尺寸
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
function setScale() {
const winW = window.innerWidth;
const winH = window.innerHeight;
// 计算宽高缩放比例,取较小值保持完整显示
const scaleX = winW / DESIGN_WIDTH;
const scaleY = winH / DESIGN_HEIGHT;
const scale = Math.min(scaleX, scaleY);
// 应用缩放
screen.style.transform = `scale(${scale})`;
// 可选:居中显示
const left = (winW - DESIGN_WIDTH * scale) / 2;
const top = (winH - DESIGN_HEIGHT * scale) / 2;
screen.style.position = 'absolute';
screen.style.left = left + 'px';
screen.style.top = top + 'px';
// 更新信息
scaleInfo.innerHTML = `
缩放比例: ${scale.toFixed(3)}<br>
窗口尺寸: ${winW}×${winH}<br>
设计稿: 1920×1080<br>
空白区域: ${Math.round((winW - DESIGN_WIDTH * scale))}×${Math.round((winH - DESIGN_HEIGHT * scale))}
`;
}
setScale();
// ===== ECharts 图表配置 =====
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
chart1.setOption({
backgroundColor: 'transparent',
grid: { top: 30, right: 20, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 12 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff' }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
}
}]
});
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
chart2.setOption({
backgroundColor: 'transparent',
grid: { top: 30, right: 20, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 12 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', formatter: '¥{value}万' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
lineStyle: { color: '#e91e63', width: 3 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
}
}]
});
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
chart3.setOption({
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 11 }
}]
});
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
chart4.setOption({
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '80%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 10,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' } },
detail: {
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 20,
offsetCenter: [0, '70%']
},
data: [{ value: 23 }]
}]
});
// 图表引用数组用于resize
const charts = [chart1, chart2, chart3, chart4];
// 防抖处理 resize
let timer;
window.addEventListener('resize', () => {
clearTimeout(timer);
timer = setTimeout(() => {
setScale();
charts.forEach(chart => chart.resize());
}, 100);
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
document.getElementById('value4').textContent = newValue + 'ms';
}, 3000);
</script>
</body>
</html>
使用说明 :将以上代码保存为
1-scale-demo.html,直接在浏览器中打开即可体验。调整窗口大小观察等比例缩放效果,注意两侧的黑边。
Demo 2: VW/VH 视口单位方案
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案2: VW/VH 方案 - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
}
/* 信息面板 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 20px;
border-radius: 8px;
z-index: 9999;
width: 320px;
font-size: 14px;
}
.info-panel h3 {
color: #81c784;
margin-bottom: 12px;
font-size: 16px;
}
.info-panel .pros-cons {
margin-bottom: 15px;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #e57373;
}
.info-panel .formula {
background: #333;
padding: 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
margin-top: 10px;
color: #ff9800;
}
/* VW/VH 布局 - 直接使用视口单位 */
.header {
/* 设计稿 100px / 1080px * 100vh */
height: 9.259vh;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
/* 设计稿 48px / 1080px * 100vh */
font-size: 4.444vh;
color: #fff;
text-shadow: 0 0 2vh rgba(79, 195, 247, 0.5);
letter-spacing: 0.7vw;
}
.main-content {
display: flex;
/* 设计稿 30px / 1080px * 100vh */
padding: 2.778vh 1.562vw;
/* 设计稿 20px / 1080px * 100vh */
gap: 1.852vh 1.042vw;
/* 总高度 100vh - header 高度 */
height: 90.741vh;
}
.sidebar {
/* 设计稿 400px / 1920px * 100vw */
width: 20.833vw;
background: rgba(15, 52, 96, 0.3);
/* 设计稿 16px / 1080px * 100vh */
border-radius: 1.481vh;
/* 设计稿 20px / 1080px * 100vh */
padding: 1.852vh;
border: 0.093vh solid rgba(79, 195, 247, 0.2);
}
.chart-area {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.852vh 1.042vw;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 1.481vh;
padding: 1.852vh;
border: 0.093vh solid rgba(79, 195, 247, 0.2);
display: flex;
flex-direction: column;
}
.card-title {
color: #4fc3f7;
/* 设计稿 24px / 1080px * 100vh */
font-size: 2.222vh;
margin-bottom: 1.389vh;
}
.card-value {
color: #fff;
/* 设计稿 56px / 1080px * 100vh */
font-size: 5.185vh;
font-weight: bold;
}
.mini-chart {
flex: 1;
min-height: 13.889vh;
margin-top: 1.5vh;
border-radius: 0.741vh;
}
/* 代码展示区域 */
.code-panel {
margin-top: 2vh;
background: rgba(0, 0, 0, 0.5);
padding: 1.5vh;
border-radius: 1vh;
font-family: 'Courier New', monospace;
font-size: 1.3vh;
color: #a5d6a7;
overflow: hidden;
}
.notice {
position: fixed;
bottom: 2vh;
left: 2vw;
right: 22vw;
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #f44336;
padding: 1.5vh 2vw;
border-radius: 0.8vh;
font-size: 1.6vh;
}
/* 问题演示:文字溢出 */
.overflow-demo {
background: rgba(244, 67, 54, 0.1);
border: 1px dashed #f44336;
padding: 1vh;
margin-top: 1vh;
font-size: 1.5vh;
}
/* 使用 CSS 变量简化计算 */
:root {
--vh: 1vh;
--vw: 1vw;
}
/* 但这不是完美的解决方案 */
.sidebar p {
color: rgba(255,255,255,0.7);
font-size: 1.667vh;
line-height: 1.8;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<h3>方案2: VW/VH 方案</h3>
<div class="pros-cons">
<div class="pros">✓ 优点:</div>
• 无 JS 依赖<br>
• 利用全部视口空间<br>
• 无黑边/留白<br><br>
<div class="cons">✗ 缺点:</div>
• 计算公式复杂<br>
• 单位换算容易出错<br>
• 字体可能过大/过小<br>
• 宽高比例难以协调
</div>
<div class="formula">
计算公式:<br>
100px / 1920px * 100vw<br>
= 5.208vw
</div>
</div>
<!-- 页面内容 -->
<div class="header">VW/VH 方案演示 - 满屏无黑边</div>
<div class="main-content">
<div class="sidebar">
<div class="card-title">左侧信息面板</div>
<p>
此方案使用 vw/vh 单位代替 px。<br><br>
虽然页面始终铺满屏幕,但计算复杂。注意右侧卡牌区域在小屏幕上文字可能显得过大。
</p>
<div class="code-panel">
/* 实际开发中的混乱 */<br>
width: 20.833vw;<br>
height: 13.889vh;<br>
font-size: 4.444vh;<br>
padding: 1.852vh 1.562vw;<br>
/* 这些数字是怎么来的? */
</div>
<div class="overflow-demo">
<strong>⚠️ 问题演示:</strong><br>
当屏幕很宽但很矮时,文字按 vh 计算变得极小,而容器按 vw 计算保持宽大,导致内容稀疏。
</div>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
<div class="notice">
⚠️ 缺点演示:调整浏览器窗口为超宽矮屏(如 2560×600),观察字体与容器比例的失调。对比 Scale 方案在这个场景的表现。
</div>
<script>
// ===== ECharts 图表配置 =====
// 图表1: 柱状图 - 实时用户数
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
const option1 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 45 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
},
animationDuration: 1500
}]
};
chart1.setOption(option1);
// 图表2: 折线图 - 交易金额趋势
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
const option2 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#e91e63', width: 3 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
},
animationDuration: 1500
}]
};
chart2.setOption(option2);
// 图表3: 饼图 - 系统负载分布
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
const option3 = {
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 10 },
labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
animationDuration: 1500
}]
};
chart3.setOption(option3);
// 图表4: 仪表盘 - 响应时间
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
const option4 = {
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '75%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 8,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
detail: {
valueAnimation: true,
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 16,
offsetCenter: [0, '65%']
},
data: [{ value: 23 }],
animationDuration: 2000
}]
};
chart4.setOption(option4);
// 响应式调整
const charts = [chart1, chart2, chart3, chart4];
window.addEventListener('resize', () => {
charts.forEach(chart => chart.resize());
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
}, 3000);
</script>
</body>
</html>
Demo 3: Rem 动态计算方案
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案3: Rem 方案 - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
// Rem 计算逻辑
(function() {
const designWidth = 1920;
function setRem() {
const winWidth = window.innerWidth;
// 以设计稿宽度为基准,100rem = 设计稿宽度
const rem = winWidth / designWidth * 100;
document.documentElement.style.fontSize = rem + 'px';
}
setRem();
let timer;
window.addEventListener('resize', function() {
clearTimeout(timer);
timer = setTimeout(setRem, 100);
});
})();
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
}
/* 信息面板 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 0.2rem;
border-radius: 0.08rem;
z-index: 9999;
width: 3.3rem;
font-size: 0.073rem;
}
.info-panel h3 {
color: #4fc3f7;
margin-bottom: 0.062rem;
font-size: 0.083rem;
}
.info-panel .pros-cons {
margin-bottom: 0.078rem;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #e57373;
}
.info-panel .current-rem {
background: #4caf50;
color: #000;
padding: 0.05rem;
border-radius: 0.04rem;
font-weight: bold;
margin-top: 0.05rem;
}
/* Rem 布局 - 1rem = 设计稿中 100px (在 1920px 宽度下) */
.header {
/* 设计稿 100px = 1rem */
height: 1rem;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
/* 设计稿 48px = 0.48rem */
font-size: 0.48rem;
color: #fff;
text-shadow: 0 0 0.1rem rgba(79, 195, 247, 0.5);
letter-spacing: 0.08rem;
}
.main-content {
display: flex;
/* 设计稿 30px = 0.3rem */
padding: 0.3rem;
/* 设计稿 20px = 0.2rem */
gap: 0.2rem;
/* 总高度 100vh - header */
min-height: calc(100vh - 1rem);
}
.sidebar {
/* 设计稿 400px = 4rem */
width: 4rem;
background: rgba(15, 52, 96, 0.3);
/* 设计稿 16px = 0.16rem */
border-radius: 0.16rem;
padding: 0.2rem;
border: 0.01rem solid rgba(79, 195, 247, 0.2);
}
.sidebar p {
color: rgba(255,255,255,0.7);
font-size: 0.18rem;
line-height: 1.8;
}
.chart-area {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.2rem;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 0.16rem;
padding: 0.2rem;
border: 0.01rem solid rgba(79, 195, 247, 0.2);
display: flex;
flex-direction: column;
}
.card-title {
color: #4fc3f7;
font-size: 0.22rem;
margin-bottom: 0.12rem;
}
.card-value {
color: #fff;
font-size: 0.56rem;
font-weight: bold;
}
.mini-chart {
flex: 1;
min-height: 1.5rem;
margin-top: 0.15rem;
border-radius: 0.08rem;
}
/* 代码展示 */
.code-panel {
margin-top: 0.25rem;
background: rgba(0, 0, 0, 0.5);
padding: 0.15rem;
border-radius: 0.1rem;
font-family: 'Courier New', monospace;
font-size: 0.13rem;
color: #a5d6a7;
}
/* 闪烁问题演示 */
.flash-demo {
margin-top: 0.2rem;
padding: 0.15rem;
background: rgba(255, 152, 0, 0.1);
border: 1px dashed #ff9800;
color: #ff9800;
font-size: 0.15rem;
}
.fouc-warning {
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #f44336;
padding: 0.15rem;
margin-top: 0.2rem;
border-radius: 0.08rem;
font-size: 0.15rem;
}
.notice {
position: fixed;
bottom: 0.2rem;
left: 0.3rem;
right: 3.6rem;
background: rgba(255, 152, 0, 0.2);
border: 1px solid #ff9800;
color: #ff9800;
padding: 0.15rem 0.2rem;
border-radius: 0.08rem;
font-size: 0.16rem;
}
/* FOUC 模拟 - 页面加载时的闪烁 */
.no-js-fallback {
display: none;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<h3>方案3: Rem 方案</h3>
<div class="pros-cons">
<div class="pros">✓ 优点:</div>
• 计算相对直观 (设计稿/100)<br>
• 兼容性好 (支持 IE)<br>
• 宽高等比缩放<br><br>
<div class="cons">✗ 缺点:</div>
• 依赖 JS 设置根字体<br>
• 页面加载可能闪烁<br>
• 高度仍需要特殊处理<br>
• 设计稿转换工作量大
</div>
<div class="current-rem" id="remInfo">
根字体: 100px<br>
(1920px 宽度下)
</div>
</div>
<!-- 页面内容 -->
<div class="header">REM 方案演示 - JS 动态计算</div>
<div class="main-content">
<div class="sidebar">
<div class="card-title">左侧信息面板</div>
<p>
1rem = 设计稿的 100px<br><br>
在 1920px 宽度的屏幕上,根字体大小为 100px,便于计算转换。
</p>
<div class="code-panel">
// JS 计算根字体 (head 中)<br>
const rem = winWidth / 1920 * 100;<br>
html.style.fontSize = rem + 'px';<br><br>
// CSS 使用<br>
width: 4rem; /* = 400px */
</div>
<div class="flash-demo">
<strong>⚠️ 闪烁问题 (FOUC):</strong><br>
如果 JS 在 head 末尾执行,页面会先按默认 16px 渲染,然后跳动到计算值。
</div>
<div class="fouc-warning">
💡 解决方案:将 rem 计算脚本放在 <head> 最前面,或使用内联 style。
</div>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
<div class="notice">
💡 修改窗口大小观察变化。注意:此方案主要处理宽度适配,高度方向元素可能会超出屏幕(尝试将窗口压得很矮)。
</div>
<script>
// 更新 rem 信息
function updateRemInfo() {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
document.getElementById('remInfo').innerHTML = `
根字体: ${rem.toFixed(2)}px<br>
窗口宽度: ${window.innerWidth}px<br>
1rem = 设计稿的 100px
`;
}
updateRemInfo();
let timer;
window.addEventListener('resize', function() {
clearTimeout(timer);
timer = setTimeout(updateRemInfo, 100);
});
// ===== ECharts 图表配置 =====
// 图表1: 柱状图 - 实时用户数
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
const option1 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 45 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
},
animationDuration: 1500
}]
};
chart1.setOption(option1);
// 图表2: 折线图 - 交易金额趋势
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
const option2 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#e91e63', width: 3 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
},
animationDuration: 1500
}]
};
chart2.setOption(option2);
// 图表3: 饼图 - 系统负载分布
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
const option3 = {
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 10 },
labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
animationDuration: 1500
}]
};
chart3.setOption(option3);
// 图表4: 仪表盘 - 响应时间
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
const option4 = {
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '75%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 8,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
detail: {
valueAnimation: true,
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 16,
offsetCenter: [0, '65%']
},
data: [{ value: 23 }],
animationDuration: 2000
}]
};
chart4.setOption(option4);
// 响应式调整
const charts = [chart1, chart2, chart3, chart4];
window.addEventListener('resize', () => {
charts.forEach(chart => chart.resize());
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
}, 3000);
</script>
</body>
</html>
Demo 4: 流式/弹性布局方案
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案4: 流式布局 - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
}
/* 信息面板 - 使用 px 保持固定大小参考 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 20px;
border-radius: 8px;
z-index: 9999;
width: 320px;
font-size: 14px;
}
.info-panel h3 {
color: #ff9800;
margin-bottom: 12px;
font-size: 16px;
}
.info-panel .pros-cons {
margin-bottom: 15px;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #e57373;
}
/* 流式布局 - 使用 % fr auto 等弹性单位 */
.header {
height: 10%; /* 百分比高度 */
min-height: 60px;
max-height: 120px;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(24px, 4vw, 48px); /* 流体字体 */
color: #fff;
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
letter-spacing: 0.5vw;
padding: 0 5%;
}
.main-content {
display: flex;
padding: 3%;
gap: 2%;
min-height: calc(90% - 60px);
}
.sidebar {
width: 25%; /* 百分比宽度 */
min-width: 200px;
max-width: 400px;
background: rgba(15, 52, 96, 0.3);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.sidebar p {
color: rgba(255,255,255,0.7);
font-size: clamp(14px, 1.5vw, 18px);
line-height: 1.8;
}
/* CSS Grid 布局 */
.chart-area {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(79, 195, 247, 0.2);
display: flex;
flex-direction: column;
min-height: 150px;
}
.card-title {
color: #4fc3f7;
font-size: clamp(16px, 2vw, 24px);
margin-bottom: 10px;
}
.card-value {
color: #fff;
font-size: clamp(28px, 4vw, 56px);
font-weight: bold;
white-space: nowrap;
}
.mini-chart {
flex: 1;
min-height: 100px;
margin-top: 10px;
border-radius: 8px;
}
/* 问题展示:数据大屏布局崩坏 */
.problem-demo {
margin-top: 20px;
padding: 15px;
background: rgba(244, 67, 54, 0.1);
border: 1px dashed #f44336;
border-radius: 8px;
}
.problem-demo h4 {
color: #f44336;
margin-bottom: 10px;
}
.problem-demo p {
color: #f44336;
font-size: 14px;
}
/* 视觉偏差对比: */
.comparison-box {
display: flex;
gap: 20px;
margin-top: 15px;
}
.fixed-ratio {
width: 100px;
height: 60px;
background: #4fc3f7;
display: flex;
align-items: center;
justify-content: center;
color: #000;
font-size: 12px;
}
.fluid-shape {
width: 20%;
padding-bottom: 12%;
background: #ff9800;
position: relative;
}
.fluid-shape span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #000;
font-size: 12px;
white-space: nowrap;
}
.notice {
position: fixed;
bottom: 20px;
left: 20px;
right: 360px;
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #f44336;
padding: 15px 20px;
border-radius: 8px;
font-size: 14px;
}
.label-tag {
display: inline-block;
background: #4caf50;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-right: 5px;
}
.label-tag.warning {
background: #ff9800;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<h3>方案4: 流式/弹性布局</h3>
<div class="pros-cons">
<div class="pros">✓ 优点:</div>
• 纯 CSS,无依赖<br>
• 自然响应式<br>
• 内容自适应<br><br>
<div class="cons">✗ 缺点:</div>
• <strong>大屏空间利用率低</strong><br>
• 图表比例难以控制<br>
• 无法精确还原设计稿<br>
• 文字/图形可能变形
</div>
<div style="margin-top: 10px; font-size: 12px; color: #999;">
适合:后台管理系统<br>
不适合:数据可视化大屏
</div>
</div>
<!-- 页面内容 -->
<div class="header">流式布局演示 - 自然伸缩</div>
<div class="main-content">
<div class="sidebar">
<div style="color: #ff9800; font-size: 18px; margin-bottom: 15px;">左侧信息面板</div>
<p>
<span class="label-tag">%</span> 百分比宽度<br>
<span class="label-tag">fr</span> Grid 弹性分配<br>
<span class="label-tag warning">clamp</span> 流体字体<br><br>
这个方案使用 CSS 的固有响应式能力。但请注意右侧卡片在宽屏上的变化------它们会无限拉宽!
</p>
<div class="problem-demo">
<h4>⚠️ 大屏场景的问题</h4>
<p>
<strong>问题1:</strong> 宽屏下卡片过度拉伸<br>
<strong>问题2:</strong> 图表比例失控<br>
<strong>问题3:</strong> 无法精确对齐设计稿像素
</p>
<div class="comparison-box">
<div class="fixed-ratio">固定比例</div>
<div class="fluid-shape"><span>20%宽度</span></div>
</div>
<p style="margin-top: 10px; font-size: 12px;">
调整窗口宽度,观察流体元素的宽高比例变化。
</p>
</div>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
<div class="notice">
❌ 调整窗口到超宽屏(≥2560px),观察右侧卡片的变形:宽高比例完全失控,图表变成"矮胖"形状。对比 Scale 方案在这个场景的表现。
</div>
<script>
// ===== ECharts 图表配置 =====
// 图表1: 柱状图 - 实时用户数
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
const option1 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 45 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
},
animationDuration: 1500
}]
};
chart1.setOption(option1);
// 图表2: 折线图 - 交易金额趋势
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
const option2 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#e91e63', width: 3 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
},
animationDuration: 1500
}]
};
chart2.setOption(option2);
// 图表3: 饼图 - 系统负载分布
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
const option3 = {
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 10 },
labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
animationDuration: 1500
}]
};
chart3.setOption(option3);
// 图表4: 仪表盘 - 响应时间
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
const option4 = {
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '75%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 8,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
detail: {
valueAnimation: true,
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 16,
offsetCenter: [0, '65%']
},
data: [{ value: 23 }],
animationDuration: 2000
}]
};
chart4.setOption(option4);
// 响应式调整
const charts = [chart1, chart2, chart3, chart4];
window.addEventListener('resize', () => {
charts.forEach(chart => chart.resize());
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
}, 3000);
</script>
</body>
</html>
Demo 5: 响应式断点方案
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案5: 响应式断点 - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
}
/* 信息面板 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 20px;
border-radius: 8px;
z-index: 9999;
width: 320px;
font-size: 14px;
max-height: 90vh;
overflow-y: auto;
}
.info-panel h3 {
color: #e91e63;
margin-bottom: 12px;
font-size: 16px;
}
.info-panel .pros-cons {
margin-bottom: 15px;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #e57373;
}
.current-breakpoint {
background: #e91e63;
color: #fff;
padding: 10px;
border-radius: 4px;
font-weight: bold;
margin-top: 10px;
font-size: 16px;
}
.breakpoint-list {
margin-top: 10px;
font-size: 12px;
}
.breakpoint-list div {
padding: 4px 0;
}
.breakpoint-list .active {
color: #e91e63;
font-weight: bold;
}
/* 断点样式定义 */
/* 默认/移动端: < 768px */
.header {
height: 50px;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
text-shadow: 0 0 10px rgba(79, 195, 247, 0.5);
letter-spacing: 2px;
}
.main-content {
display: flex;
flex-direction: column;
padding: 10px;
gap: 15px;
}
.sidebar {
width: 100%;
background: rgba(15, 52, 96, 0.3);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.sidebar p {
color: rgba(255,255,255,0.7);
font-size: 14px;
line-height: 1.6;
}
.chart-area {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.card-title {
color: #4fc3f7;
font-size: 16px;
margin-bottom: 8px;
}
.card-value {
color: #fff;
font-size: 28px;
font-weight: bold;
}
.mini-chart {
height: 100px;
margin-top: 10px;
border-radius: 4px;
}
/* 平板: 768px - 1200px */
@media (min-width: 768px) {
.header {
height: 70px;
font-size: 24px;
letter-spacing: 4px;
}
.main-content {
flex-direction: row;
padding: 20px;
}
.sidebar {
width: 280px;
padding: 20px;
}
.chart-area {
grid-template-columns: repeat(2, 1fr);
flex: 1;
}
.card-title {
font-size: 18px;
}
.card-value {
font-size: 32px;
}
.mini-chart {
height: 100px;
}
}
/* 代码体积问题展示 */
@media (min-width: 1200px) {
.header {
height: 80px;
font-size: 32px;
letter-spacing: 6px;
}
.main-content {
padding: 25px;
gap: 20px;
}
.sidebar {
width: 350px;
}
.card-title {
font-size: 20px;
margin-bottom: 10px;
}
.card-value {
font-size: 40px;
}
.mini-chart {
height: 120px;
margin-top: 15px;
}
}
/* 大屏幕: 1920px - 2560px */
@media (min-width: 1920px) {
.header {
height: 100px;
font-size: 40px;
letter-spacing: 8px;
}
.main-content {
padding: 30px;
}
.sidebar {
width: 400px;
}
.sidebar p {
font-size: 16px;
}
.card-value {
font-size: 48px;
}
.mini-chart {
height: 150px;
}
}
/* 超大屏幕: > 2560px */
@media (min-width: 2560px) {
.header {
height: 120px;
font-size: 48px;
}
.chart-area {
grid-template-columns: repeat(4, 1fr);
}
.card-value {
font-size: 56px;
}
}
/* 代码体积问题展示 */
.code-stats {
margin-top: 15px;
padding: 10px;
background: rgba(244, 67, 54, 0.1);
border: 1px dashed #f44336;
border-radius: 4px;
}
.code-stats h4 {
color: #f44336;
margin-bottom: 5px;
}
.code-stats .stat {
display: flex;
justify-content: space-between;
font-size: 12px;
padding: 3px 0;
}
.notice {
position: fixed;
bottom: 20px;
left: 20px;
right: 360px;
background: rgba(233, 30, 99, 0.2);
border: 1px solid #e91e63;
color: #e91e63;
padding: 15px 20px;
border-radius: 8px;
font-size: 14px;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<h3>方案5: 响应式断点</h3>
<div class="pros-cons">
<div class="pros">✓ 优点:</div>
• 精确控制各分辨率<br>
• 字体可读性可控<br>
• 适合固定规格大屏<br><br>
<div class="cons">✗ 缺点:</div>
• <strong>代码量爆炸</strong><br>
• 维护困难<br>
• 无法覆盖所有尺寸<br>
• CSS 文件体积大
</div>
<div class="current-breakpoint" id="currentBP">
当前断点: 默认/移动端
</div>
<div class="breakpoint-list">
<div data-bp="0">< 768px: 移动端</div>
<div data-bp="1">768px - 1200px: 平板</div>
<div data-bp="2">1200px - 1920px: 桌面</div>
<div data-bp="3">1920px - 2560px: 大屏</div>
<div data-bp="4">> 2560px: 超大屏</div>
</div>
<div class="code-stats">
<h4>⚠️ 维护成本</h4>
<div class="stat">
<span>每个组件</span>
<span>×5 套样式</span>
</div>
<div class="stat">
<span>10个组件</span>
<span>= 50套样式</span>
</div>
<div class="stat">
<span>新增分辨率</span>
<span>全文件修改</span>
</div>
</div>
</div>
<!-- 页面内容 -->
<div class="header">响应式断点演示 - @media 查询</div>
<div class="main-content">
<div class="sidebar">
<div style="color: #e91e63; font-size: 20px; margin-bottom: 15px;">左侧信息面板</div>
<p>
调整窗口宽度,观察布局在不同断点的变化。<br><br>
<strong>可能遇到的问题:</strong><br>
• 1366px 和 1440px 怎么办?<br>
• 3840px(4K)应该如何显示?<br>
• 修改一个组件要改 5 处?
</p>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
<div class="notice">
🔧 拖动窗口宽度观察布局变化。注意:即使 "桌面" 断点(1920px)也不是精确还原设计稿,只是 "看起来差不多"。
</div>
<script>
// 检测当前断点
const breakpoints = [
{ name: '默认/移动端', max: 768 },
{ name: '平板', max: 1200 },
{ name: '桌面', max: 1920 },
{ name: '大屏', max: 2560 },
{ name: '超大屏', max: Infinity }
];
function detectBreakpoint() {
const width = window.innerWidth;
let activeIndex = 0;
for (let i = 0; i < breakpoints.length; i++) {
if (width < breakpoints[i].max) {
activeIndex = i;
break;
}
activeIndex = i;
}
document.getElementById('currentBP').textContent =
`当前断点: ${breakpoints[activeIndex].name} (${width}px)`;
// 高亮断点列表
document.querySelectorAll('.breakpoint-list div').forEach((div, index) => {
div.classList.toggle('active', index === activeIndex);
});
}
detectBreakpoint();
let timer;
window.addEventListener('resize', () => {
clearTimeout(timer);
timer = setTimeout(detectBreakpoint, 100);
});
// ===== ECharts 图表配置 =====
// 图表1: 柱状图 - 实时用户数
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
const option1 = {
backgroundColor: 'transparent',
grid: { top: 25, right: 10, bottom: 20, left: 40 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 9 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 9 }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
},
animationDuration: 1500
}]
};
chart1.setOption(option1);
// 图表2: 折线图 - 交易金额趋势
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
const option2 = {
backgroundColor: 'transparent',
grid: { top: 25, right: 10, bottom: 20, left: 45 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 9 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 9, formatter: '¥{value}' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
symbol: 'circle',
symbolSize: 5,
lineStyle: { color: '#e91e63', width: 2 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
},
animationDuration: 1500
}]
};
chart2.setOption(option2);
// 图表3: 饼图 - 系统负载分布
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
const option3 = {
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['30%', '60%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 9 },
labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
animationDuration: 1500
}]
};
chart3.setOption(option3);
// 图表4: 仪表盘 - 响应时间
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
const option4 = {
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '70%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 6,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' }, width: 3 },
axisTick: { distance: -6, length: 3, lineStyle: { color: '#fff' } },
splitLine: { distance: -6, length: 8, lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff', distance: -15, fontSize: 8 },
detail: {
valueAnimation: true,
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 12,
offsetCenter: [0, '60%']
},
data: [{ value: 23 }],
animationDuration: 2000
}]
};
chart4.setOption(option4);
// 响应式调整
const charts = [chart1, chart2, chart3, chart4];
window.addEventListener('resize', () => {
charts.forEach(chart => chart.resize());
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
}, 3000);
</script>
</body>
</html>
Demo 6: 混合方案(推荐)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案6: 混合方案(推荐) - 大屏适配方案对比</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
// 混合方案:Scale + Rem
(function() {
const designWidth = 1920;
const designHeight = 1080;
const minScale = 0.6; // 最小缩放限制,防止过小
function adapt() {
const winW = window.innerWidth;
const winH = window.innerHeight;
// Scale 计算
const scaleX = winW / designWidth;
const scaleY = winH / designHeight;
const scale = Math.max(Math.min(scaleX, scaleY), minScale);
// 应用 scale
const screen = document.getElementById('screen');
if (screen) {
screen.style.transform = `scale(${scale})`;
}
// Rem 计算 - 根据缩放比例调整根字体
// 当 scale < 1 时,增加根字体补偿
const baseRem = 100;
const fontScale = Math.max(scale, minScale);
const rem = baseRem * fontScale;
document.documentElement.style.fontSize = rem + 'px';
// 更新信息面板
updateInfo(scale, rem, winW, winH);
}
function updateInfo(scale, rem, winW, winH) {
const info = document.getElementById('mixedInfo');
if (info) {
info.innerHTML = `
Scale: ${scale.toFixed(3)}<br>
Rem: ${rem.toFixed(1)}px<br>
窗口: ${winW}×${winH}<br>
最小限制: ${minScale}
`;
}
}
// 页面加载前执行
adapt();
// 防抖 resize
let timer;
window.addEventListener('resize', () => {
clearTimeout(timer);
timer = setTimeout(adapt, 100);
});
// 暴露全局供切换模式使用
window.adaptMode = 'mixed'; // mixed, scale-only, rem-only
window.setAdaptMode = function(mode) {
window.adaptMode = mode;
const screen = document.getElementById('screen');
const designWidth = 1920;
const designHeight = 1080;
const winW = window.innerWidth;
const winH = window.innerHeight;
if (mode === 'scale-only') {
const scale = Math.min(winW / designWidth, winH / designHeight);
screen.style.transform = `scale(${scale})`;
document.documentElement.style.fontSize = '100px';
} else if (mode === 'rem-only') {
screen.style.transform = 'none';
const rem = winW / designWidth * 100;
document.documentElement.style.fontSize = rem + 'px';
} else {
adapt();
}
};
// 初始化覆盖
window.setAdaptMode('mixed');
})();
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
overflow: hidden;
background: #0a0a1a;
}
/* 信息面板 - 固定不缩放 */
.info-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 20px;
border-radius: 8px;
z-index: 9999;
width: 340px;
font-size: 14px;
border: 1px solid #4caf50;
}
.info-panel h3 {
color: #4caf50;
margin-bottom: 12px;
font-size: 16px;
}
.info-panel .recommend {
background: #4caf50;
color: #000;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
display: inline-block;
margin-bottom: 10px;
}
.info-panel .pros-cons {
margin-bottom: 15px;
}
.info-panel .pros {
color: #81c784;
}
.info-panel .cons {
color: #fff176;
}
.mode-switcher {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #333;
}
.mode-switcher h4 {
color: #4fc3f7;
margin-bottom: 10px;
}
.mode-btn {
display: block;
width: 100%;
padding: 8px 12px;
margin-bottom: 6px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
text-align: left;
font-size: 12px;
}
.mode-btn:hover {
background: #444;
}
.mode-btn.active {
background: #4caf50;
border-color: #4caf50;
color: #000;
}
.info-value {
background: #2196f3;
color: #fff;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
/* 大屏容器 */
.screen-container {
width: 1920px;
height: 1080px;
transform-origin: 0 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
position: absolute;
overflow: hidden;
top: 0;
left: 0;
}
/* 居中显示 */
.centered {
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
}
/* 大屏内容样式 - 使用 rem */
.header {
height: 1rem;
background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.5rem;
color: #fff;
text-shadow: 0 0 0.1rem rgba(79, 195, 247, 0.5);
letter-spacing: 0.08rem;
}
.main-content {
display: flex;
padding: 0.3rem;
gap: 0.2rem;
height: calc(100% - 1rem);
}
.sidebar {
width: 4rem;
background: rgba(15, 52, 96, 0.3);
border-radius: 0.16rem;
padding: 0.2rem;
border: 0.01rem solid rgba(79, 195, 247, 0.2);
}
.sidebar p {
color: rgba(255,255,255,0.7);
font-size: 0.18rem;
line-height: 1.8;
}
.chart-area {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.2rem;
}
.card {
background: rgba(15, 52, 96, 0.3);
border-radius: 0.16rem;
padding: 0.2rem;
border: 0.01rem solid rgba(79, 195, 247, 0.2);
display: flex;
flex-direction: column;
}
.card-title {
color: #4fc3f7;
font-size: 0.22rem;
margin-bottom: 0.08rem;
}
.card-value {
color: #fff;
font-size: 0.56rem;
font-weight: bold;
}
.mini-chart {
flex: 1;
min-height: 1.5rem;
margin-top: 0.15rem;
border-radius: 0.08rem;
}
/* 特性说明卡片 */
.feature-card {
margin-top: 0.15rem;
padding: 0.15rem;
background: rgba(76, 175, 80, 0.1);
border: 1px solid #4caf50;
border-radius: 0.1rem;
}
.feature-card h4 {
color: #4caf50;
font-size: 0.18rem;
margin-bottom: 0.05rem;
}
.feature-card p {
font-size: 0.14rem;
color: rgba(255,255,255,0.8);
}
/* 对比指示器 */
.compare-indicator {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.9);
padding: 15px 20px;
border-radius: 8px;
color: #fff;
border-left: 4px solid #4caf50;
}
.compare-indicator h4 {
color: #4caf50;
margin-bottom: 5px;
}
</style>
</head>
<body>
<!-- 信息面板 -->
<div class="info-panel">
<div class="recommend">⭐ 生产环境推荐</div>
<h3>方案6: 混合方案</h3>
<div class="pros-cons">
<div class="pros">✓ 结合 Scale + Rem 优点</div>
• 等比例缩放保证布局<br>
• 字体最小值防止过小<br>
• 大屏清晰、小屏可读<br><br>
<div class="cons">△ 注意:</div>
• 需要 JS 支持<br>
• 计算逻辑稍复杂
</div>
<div class="mode-switcher">
<h4>模式切换对比:</h4>
<button class="mode-btn active" onclick="switchMode('mixed', this)">
🌟 混合方案 (推荐)
</button>
<button class="mode-btn" onclick="switchMode('scale-only', this)">
📐 纯 Scale (字体过小)
</button>
<button class="mode-btn" onclick="switchMode('rem-only', this)">
🔤 纯 Rem (可能变形)
</button>
</div>
<div class="info-value" id="mixedInfo">
Scale: 1.0<br>
Rem: 100px<br>
窗口: 1920×1080
</div>
</div>
<!-- 对比指示器 -->
<div class="compare-indicator">
<h4>💡 对比技巧</h4>
<p>1. 切换到"纯 Scale",缩小窗口,观察字体变小</p>
<p>2. 切换回"混合方案",字体有最小值限制</p>
<p>3. 调整到4K屏,观察布局比例保持与设计稿一致</p>
</div>
<!-- 大屏容器 -->
<div class="screen-container centered" id="screen">
<div class="header">混合方案演示 - Scale + Rem 双重保障</div>
<div class="main-content">
<div class="sidebar">
<div style="color: #4caf50; font-size: 0.24rem; margin-bottom: 0.15rem;">混合方案说明</div>
<p>
此方案结合 Scale 的视觉一致性 和 Rem 的灵活性。<br><br>
<strong>核心算法:</strong><br>
1. 计算 screen 的 scale 比例<br>
2. 根字体 = baseFont * max(scale, minLimit)<br>
3. 所有尺寸使用 rem 单位<br><br>
这样既保持设计稿比例,又确保文字可读。
</p>
<div class="feature-card">
<h4>🎯 最小字体保护</h4>
<p>当屏幕缩小时,字体不会无限缩小,保证基本可读性。</p>
</div>
<div class="feature-card">
<h4>📐 严格比例保持</h4>
<p>图表、卡片的宽高比例严格遵循设计稿,无变形。</p>
</div>
</div>
<div class="chart-area">
<div class="card">
<div class="card-title">实时用户数</div>
<div class="card-value">128,456</div>
<div class="mini-chart" id="chart1"></div>
</div>
<div class="card">
<div class="card-title">交易金额</div>
<div class="card-value">¥2.3M</div>
<div class="mini-chart" id="chart2"></div>
</div>
<div class="card">
<div class="card-title">系统负载</div>
<div class="card-value">68%</div>
<div class="mini-chart" id="chart3"></div>
</div>
<div class="card">
<div class="card-title">响应时间</div>
<div class="card-value">23ms</div>
<div class="mini-chart" id="chart4"></div>
</div>
</div>
</div>
</div>
<script>
function switchMode(mode, btn) {
window.setAdaptMode(mode);
// 更新按钮状态
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 更新位置信息
updatePosition();
}
function updatePosition() {
const screen = document.getElementById('screen');
const winW = window.innerWidth;
const winH = window.innerHeight;
const designW = 1920;
const designH = 1080;
// 获取当前 scale
const transform = getComputedStyle(screen).transform;
let scale = 1;
if (transform && transform !== 'none') {
const matrix = transform.match(/matrix\(([^)]+)\)/);
if (matrix) {
const values = matrix[1].split(',').map(parseFloat);
scale = values[0];
}
}
// 居中计算
const left = (winW - designW * scale) / 2;
const top = (winH - designH * scale) / 2;
screen.style.left = Math.max(0, left) + 'px';
screen.style.top = Math.max(0, top) + 'px';
}
// 初始化位置
window.addEventListener('load', updatePosition);
window.addEventListener('resize', updatePosition);
// ===== ECharts 图表配置 =====
// 图表1: 柱状图 - 实时用户数
const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
const option1 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 45 },
xAxis: {
type: 'category',
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
type: 'bar',
data: [32000, 28000, 85000, 120000, 98000, 128456],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4fc3f7' },
{ offset: 1, color: '#2196f3' }
]),
borderRadius: [4, 4, 0, 0]
},
animationDuration: 1500
}]
};
chart1.setOption(option1);
// 图表2: 折线图 - 交易金额趋势
const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
const option2 = {
backgroundColor: 'transparent',
grid: { top: 30, right: 15, bottom: 25, left: 50 },
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
},
series: [{
type: 'line',
data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#e91e63', width: 3 },
itemStyle: { color: '#e91e63' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
{ offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
])
},
animationDuration: 1500
}]
};
chart2.setOption(option2);
// 图表3: 饼图 - 系统负载分布
const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
const option3 = {
backgroundColor: 'transparent',
series: [{
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '55%'],
data: [
{ value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
{ value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
{ value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
{ value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
],
label: { color: '#fff', fontSize: 10 },
labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
animationDuration: 1500
}]
};
chart3.setOption(option3);
// 图表4: 仪表盘 - 响应时间
const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
const option4 = {
backgroundColor: 'transparent',
series: [{
type: 'gauge',
radius: '75%',
center: ['50%', '55%'],
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 8,
color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
}
},
pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
detail: {
valueAnimation: true,
formatter: '{value}ms',
color: '#4fc3f7',
fontSize: 16,
offsetCenter: [0, '65%']
},
data: [{ value: 23 }],
animationDuration: 2000
}]
};
chart4.setOption(option4);
// 响应式调整
const charts = [chart1, chart2, chart3, chart4];
window.addEventListener('resize', () => {
charts.forEach(chart => chart.resize());
});
// 模拟数据更新
setInterval(() => {
const newValue = Math.floor(Math.random() * 20) + 15;
chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
}, 3000);
</script>
</body>
</html>
css
/* 使用 rem 单位编写样式 */
.header {
height: 1rem; /* 设计稿 100px */
font-size: 0.5rem; /* 设计稿 50px */
letter-spacing: 0.08rem;
}
.sidebar {
width: 4rem; /* 设计稿 400px */
padding: 0.2rem; /* 设计稿 20px */
}
.card-title {
font-size: 0.22rem; /* 设计稿 22px */
}
.card-value {
font-size: 0.56rem; /* 设计稿 56px */
}
完整 Demo 文件列表:
demo1- Scale 等比例缩放(上方已提供完整代码)demo2- VW/VH 视口单位方案demo3- Rem 动态计算方案demo4- 流式/弹性布局方案demo5- 响应式断点方案demo6- 混合方案(推荐)
以上所有demo均可在本地环境中直接运行,建议按顺序体验对比各方案的表现差异
核心代码示例
Scale 方案核心代码
html
<!DOCTYPE html>
<html>
<head>
<style>
.screen-container {
width: 1920px;
height: 1080px;
transform-origin: 0 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
</style>
</head>
<body>
<div class="screen-container" id="screen">
<!-- 大屏内容 -->
</div>
<script>
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
function setScale() {
const scaleX = window.innerWidth / DESIGN_WIDTH;
const scaleY = window.innerHeight / DESIGN_HEIGHT;
const scale = Math.min(scaleX, scaleY);
const screen = document.getElementById('screen');
screen.style.transform = `scale(${scale})`;
// 居中显示
const left = (window.innerWidth - DESIGN_WIDTH * scale) / 2;
const top = (window.innerHeight - DESIGN_HEIGHT * scale) / 2;
screen.style.left = left + 'px';
screen.style.top = top + 'px';
}
setScale();
window.addEventListener('resize', setScale);
</script>
</body>
</html>
混合方案核心代码
javascript
// 混合方案:Scale + Rem
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
const MIN_SCALE = 0.6; // 最小缩放限制,防止过小
function adapt() {
const winW = window.innerWidth;
const winH = window.innerHeight;
// Scale 计算
const scaleX = winW / DESIGN_WIDTH;
const scaleY = winH / DESIGN_HEIGHT;
const scale = Math.max(Math.min(scaleX, scaleY), MIN_SCALE);
// 应用 scale
const screen = document.getElementById('screen');
screen.style.transform = `scale(${scale})`;
// Rem 计算 - 根据缩放比例调整根字体
// 当 scale < 1 时,增加根字体补偿
const baseRem = 100;
const fontScale = Math.max(scale, MIN_SCALE);
const rem = baseRem * fontScale;
document.documentElement.style.fontSize = rem + 'px';
}
adapt();
window.addEventListener('resize', adapt);
总结
大屏适配没有银弹,每种方案都有其适用场景:
- 简单大屏项目:使用 Scale 方案,快速实现
- 内容管理系统:使用流式布局,灵活适配
- 移动端优先:使用 Rem 方案,成熟稳定
- 多端统一:使用混合方案,兼顾体验
选择方案时需要综合考虑:
- 设计稿还原要求
- 目标设备规格
- 开发维护成本
- 团队技术栈
希望本文能帮助你在大屏开发中游刃有余!
如果觉得有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!